refactor: 客户端目录统一添加 kenaito-conf- 前缀
- go-client → kenaito-conf-go-client - python-client → kenaito-conf-python-client - python-client-demo → kenaito-conf-python-client-demo - nodejs-client → kenaito-conf-nodejs-client - nodejs-client-demo → kenaito-conf-nodejs-client-demo - 更新所有 README 和 package.json 中的路径引用
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const CACHE_FILE = 'config-cache.json';
|
||||
|
||||
export class Cache {
|
||||
constructor(cacheDir = '.') {
|
||||
this.cacheDir = cacheDir;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
const filePath = path.join(this.cacheDir, CACHE_FILE);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
read() {
|
||||
const filePath = path.join(this.cacheDir, CACHE_FILE);
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Config } from './config.js';
|
||||
import { Cache } from './cache.js';
|
||||
import { Listener } from './listener.js';
|
||||
import { decryptConfig } from './crypto.js';
|
||||
|
||||
export class ConfigClient {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.cache = new Cache(config.cacheDir);
|
||||
this.listener = new Listener();
|
||||
this.configs = {};
|
||||
this.secret = '';
|
||||
this.globalSecret = '';
|
||||
this.lastTime = 0;
|
||||
this.started = false;
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
onChange(handler) {
|
||||
this.listener.onChange(handler);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.configs[key] ?? null;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return { ...this.configs };
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
|
||||
try {
|
||||
await this.fetchAll();
|
||||
} catch (err) {
|
||||
console.warn('[ConfigClient] fetch all configs error:', err.message, ', trying cache');
|
||||
const cached = this.cache.read();
|
||||
if (cached) {
|
||||
this.applyConfigs(cached);
|
||||
console.info('[ConfigClient] loaded', Object.keys(this.configs).length, 'configs from cache');
|
||||
} else {
|
||||
throw new Error(`fetch configs failed and no cache: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.fetchChanged().catch((err) => {
|
||||
console.error('[ConfigClient] sync error:', err);
|
||||
});
|
||||
}, this.config.syncInterval * 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAll() {
|
||||
const url = `${this.config.baseUrl}/api/fetchAllConfigs`;
|
||||
const payload = {
|
||||
appName: this.config.appName,
|
||||
env: this.config.env,
|
||||
version: this.config.version,
|
||||
ignoreGlobal: this.config.ignoreGlobal,
|
||||
token: this.config.token,
|
||||
};
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP ${resp.status}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (data.code != null && data.code !== 0) {
|
||||
throw new Error(`server error: ${data.msg || 'unknown'}`);
|
||||
}
|
||||
|
||||
const secret = data['kenaito.config.encrypt-secret'];
|
||||
if (secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
const globalSecret = data['kenaito.config.global-encrypt-secret'];
|
||||
if (globalSecret) {
|
||||
this.globalSecret = globalSecret;
|
||||
}
|
||||
const ts = data['latest.fetch.timestamp'];
|
||||
if (ts) {
|
||||
this.lastTime = parseInt(ts, 10);
|
||||
}
|
||||
|
||||
this.cache.write(data);
|
||||
this.applyConfigs(data);
|
||||
console.info('[ConfigClient] fetched', Object.keys(this.configs).length, 'configs');
|
||||
}
|
||||
|
||||
async fetchChanged() {
|
||||
const url = `${this.config.baseUrl}/api/fetchChangedConfigs`;
|
||||
const payload = {
|
||||
appName: this.config.appName,
|
||||
env: this.config.env,
|
||||
version: this.config.version,
|
||||
lastTime: this.lastTime,
|
||||
ignoreGlobal: this.config.ignoreGlobal,
|
||||
token: this.config.token,
|
||||
};
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP ${resp.status}`);
|
||||
}
|
||||
const result = await resp.json();
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`server error code: ${result.code}`);
|
||||
}
|
||||
|
||||
const changedData = result.data || {};
|
||||
const ts = changedData['latest.fetch.timestamp'];
|
||||
if (ts) {
|
||||
this.lastTime = parseInt(ts, 10);
|
||||
}
|
||||
delete changedData['latest.fetch.timestamp'];
|
||||
|
||||
const changed = {};
|
||||
for (const [k, v] of Object.entries(changedData)) {
|
||||
changed[k] = this.tryDecrypt(v);
|
||||
}
|
||||
|
||||
Object.assign(this.configs, changed);
|
||||
this.listener.notify(changed);
|
||||
console.info('[ConfigClient]', Object.keys(changed).length, 'configs changed');
|
||||
}
|
||||
|
||||
applyConfigs(data) {
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (k.startsWith('kenaito.') || k === 'latest.fetch.timestamp') {
|
||||
continue;
|
||||
}
|
||||
if (typeof v !== 'string') {
|
||||
continue;
|
||||
}
|
||||
this.configs[k] = this.tryDecrypt(v);
|
||||
}
|
||||
}
|
||||
|
||||
tryDecrypt(value) {
|
||||
let decrypted = decryptConfig(this.secret, value);
|
||||
if (decrypted === value && this.globalSecret) {
|
||||
decrypted = decryptConfig(this.globalSecret, value);
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
export { Config } from './config.js';
|
||||
@@ -0,0 +1,12 @@
|
||||
export class Config {
|
||||
constructor(options = {}) {
|
||||
this.baseUrl = options.baseUrl || '';
|
||||
this.appName = options.appName || '';
|
||||
this.env = options.env || 'dev';
|
||||
this.version = options.version || 'latest';
|
||||
this.token = options.token || '';
|
||||
this.syncInterval = options.syncInterval || 60;
|
||||
this.ignoreGlobal = options.ignoreGlobal || false;
|
||||
this.cacheDir = options.cacheDir || '.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const CRYPT_PREFIX = '{Cipher}';
|
||||
|
||||
export function decryptConfig(secret, data) {
|
||||
if (!data.startsWith(CRYPT_PREFIX)) {
|
||||
return data;
|
||||
}
|
||||
const encrypted = data.slice(CRYPT_PREFIX.length);
|
||||
return decryptWithAES(secret, encrypted);
|
||||
}
|
||||
|
||||
export function decryptWithAES(key, data) {
|
||||
const md5Hash = crypto.createHash('md5').update(key).digest('hex');
|
||||
const aesKey = md5Hash.slice(16);
|
||||
const cipherText = Buffer.from(data, 'base64');
|
||||
const iv = Buffer.alloc(16, 0);
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
decipher.setAutoPadding(false);
|
||||
let plainText = Buffer.concat([decipher.update(cipherText), decipher.final()]);
|
||||
const padding = plainText[plainText.length - 1];
|
||||
plainText = plainText.slice(0, plainText.length - padding);
|
||||
return plainText.toString('utf8');
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ConfigClient, Config } from './client.js';
|
||||
@@ -0,0 +1,19 @@
|
||||
export class Listener {
|
||||
constructor() {
|
||||
this.handlers = [];
|
||||
}
|
||||
|
||||
onChange(handler) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
notify(changed) {
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
handler(changed);
|
||||
} catch (err) {
|
||||
console.error('[ConfigClient] change handler error:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user