@alacard-project/config-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VaultClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class VaultClient {
9
+ http;
10
+ options;
11
+ token = null;
12
+ tokenExpiry = 0;
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.http = axios_1.default.create({
16
+ baseURL: `${options.address}/v1`,
17
+ });
18
+ }
19
+ async login() {
20
+ if (this.token && Date.now() < this.tokenExpiry) {
21
+ return;
22
+ }
23
+ try {
24
+ const response = await this.http.post('/auth/approle/login', {
25
+ role_id: this.options.roleId,
26
+ secret_id: this.options.secretId,
27
+ });
28
+ const { client_token, lease_duration } = response.data.auth;
29
+ this.token = client_token;
30
+ // Buffer of 60 seconds for token renewal
31
+ this.tokenExpiry = Date.now() + (lease_duration - 60) * 1000;
32
+ this.http.defaults.headers.common['X-Vault-Token'] = this.token;
33
+ console.log('[VaultSDK] Successfully authenticated with AppRole');
34
+ }
35
+ catch (error) {
36
+ throw new Error(`[VaultSDK] Login failed: ${error.response?.data?.errors?.[0] || error.message}`);
37
+ }
38
+ }
39
+ async getKVSecrets(path) {
40
+ await this.login();
41
+ try {
42
+ const response = await this.http.get(`/secret/data/${path}`);
43
+ return response.data.data.data;
44
+ }
45
+ catch (error) {
46
+ if (error.response?.status === 404) {
47
+ console.warn(`[VaultSDK] Secret not found at path: ${path}`);
48
+ return {};
49
+ }
50
+ throw new Error(`[VaultSDK] Failed to fetch secrets: ${error.message}`);
51
+ }
52
+ }
53
+ async issueCertificate(commonName) {
54
+ await this.login();
55
+ const pkiPath = this.options.pkiPath || 'pki/issue/config-service';
56
+ try {
57
+ const response = await this.http.post(pkiPath, {
58
+ common_name: commonName,
59
+ });
60
+ const { ca_chain, certificate, private_key } = response.data.data;
61
+ return {
62
+ ca: ca_chain[0] || '', // Use the first CA in the chain
63
+ certificate,
64
+ privateKey: private_key,
65
+ };
66
+ }
67
+ catch (error) {
68
+ throw new Error(`[VaultSDK] Failed to issue certificate: ${error.message}`);
69
+ }
70
+ }
71
+ }
72
+ exports.VaultClient = VaultClient;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@alacard-project/config-sdk",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Standalone gRPC-based Configuration SDK for Alacard microservices",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "gen:proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=src/proto --proto_path=proto proto/config.proto --ts_proto_opt=outputServices=grpc-js,env=node,useOptionals=messages,exportCommonSymbols=false,esModuleInterop=true",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "dependencies": {
14
+ "@grpc/grpc-js": "^1.14.3",
15
+ "@grpc/proto-loader": "^0.8.0",
16
+ "kafkajs": "^2.2.4",
17
+ "dotenv": "^16.4.6",
18
+ "axios": "^1.7.9"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.5.6",
22
+ "@types/node": "^20.15.0",
23
+ "ts-proto": "^2.2.0"
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ syntax = "proto3";
2
+
3
+ package config;
4
+
5
+ service ConfigService {
6
+ rpc GetConfig (GetConfigRequest) returns (ConfigResponse);
7
+ rpc SetConfig (SetConfigRequest) returns (ConfigResponse);
8
+ rpc ListConfigs (ListConfigsRequest) returns (ListConfigsResponse);
9
+ }
10
+
11
+ message GetConfigRequest {
12
+ string serviceName = 1;
13
+ string environment = 2;
14
+ string version = 3;
15
+ }
16
+
17
+ message ConfigResponse {
18
+ map<string, string> values = 1;
19
+ }
20
+
21
+ message SetConfigRequest {
22
+ string serviceName = 1;
23
+ string environment = 2;
24
+ string key = 3;
25
+ string value = 4;
26
+ }
27
+
28
+ message ListConfigsRequest {
29
+ string environment = 1;
30
+ }
31
+
32
+ message ServiceConfig {
33
+ string serviceName = 1;
34
+ map<string, string> values = 2;
35
+ }
36
+
37
+ message ListConfigsResponse {
38
+ repeated ServiceConfig configs = 1;
39
+ }
@@ -0,0 +1,180 @@
1
+ import * as grpc from '@grpc/grpc-js';
2
+ import { Kafka, Consumer } from 'kafkajs';
3
+ import * as path from 'path';
4
+ import * as dotenv from 'dotenv';
5
+ import { ConfigServiceClient } from './proto/config';
6
+ import { GetConfigRequest } from './proto/config';
7
+ import { VaultClient, VaultOptions } from './vault-client';
8
+
9
+ export { VaultOptions };
10
+
11
+ export interface ConfigOptions {
12
+ serviceName: string;
13
+ environment: string;
14
+ grpcUrl: string;
15
+ version?: string;
16
+ kafkaBrokers?: string[];
17
+ useDotenvFallback?: boolean;
18
+ internalKey?: string;
19
+ tls?: {
20
+ rootCert: Buffer;
21
+ clientCert: Buffer;
22
+ clientKey: Buffer;
23
+ };
24
+ vault?: VaultOptions;
25
+ }
26
+
27
+ export class ConfigClient {
28
+ private configMap: Record<string, string> = {};
29
+ private consumer: Consumer | null = null;
30
+ private options: ConfigOptions;
31
+ private vaultClient: VaultClient | null = null;
32
+ private static instance: ConfigClient | null = null;
33
+
34
+ constructor(options: ConfigOptions) {
35
+ this.options = options;
36
+ }
37
+
38
+ static async initialize(options: ConfigOptions): Promise<ConfigClient> {
39
+ const client = new ConfigClient(options);
40
+ await client.init();
41
+ this.instance = client;
42
+ return client;
43
+ }
44
+
45
+ static getInstance(): ConfigClient {
46
+ if (!this.instance) {
47
+ throw new Error('[ConfigSDK] ConfigClient not initialized. Call initialize() first.');
48
+ }
49
+ return this.instance;
50
+ }
51
+
52
+ private async init(): Promise<void> {
53
+ const { serviceName, environment, grpcUrl, version = 'v1', useDotenvFallback = true } = this.options;
54
+
55
+ try {
56
+ // 1. Fallback to .env
57
+ if (useDotenvFallback) {
58
+ const envFile = `.env.${environment}`;
59
+ dotenv.config({ path: path.resolve(process.cwd(), envFile) });
60
+
61
+ const ALLOWED_ENV_PREFIXES = ['APP_', 'KAFKA_', 'DB_', 'REDIS_', 'GRPC_', 'PORT', 'NODE_ENV'];
62
+ for (const [key, value] of Object.entries(process.env)) {
63
+ if (value && ALLOWED_ENV_PREFIXES.some(prefix => key.startsWith(prefix))) {
64
+ this.configMap[key] = value;
65
+ }
66
+ }
67
+ }
68
+
69
+ // 2. Load from Vault (KV)
70
+ if (this.options.vault) {
71
+ this.vaultClient = new VaultClient(this.options.vault);
72
+ const vaultSecrets = await this.vaultClient.getKVSecrets(`config-service/${serviceName}`);
73
+ Object.assign(this.configMap, vaultSecrets);
74
+ console.log(`[ConfigSDK] Loaded secrets from Vault KV for ${serviceName}`);
75
+
76
+ // 3. Issue mTLS Certificates from Vault PKI if enabled
77
+ if (this.options.vault.pkiPath && !this.options.tls) {
78
+ const certs = await this.vaultClient.issueCertificate(serviceName);
79
+ this.options.tls = {
80
+ rootCert: Buffer.from(certs.ca),
81
+ clientCert: Buffer.from(certs.certificate),
82
+ clientKey: Buffer.from(certs.privateKey),
83
+ };
84
+ console.log(`[ConfigSDK] Dynamic mTLS certificate issued from Vault PKI`);
85
+ }
86
+ }
87
+
88
+ // 4. Fetch additional config via gRPC
89
+ await this.fetchRemoteConfig(serviceName, environment, grpcUrl, version);
90
+
91
+ // 5. Start watching for changes via Kafka
92
+ if (this.options.kafkaBrokers && this.options.kafkaBrokers.length > 0) {
93
+ await this.startWatching();
94
+ }
95
+ } catch (error: any) {
96
+ console.error(`[ConfigSDK] Initialization failed: ${error.message}`);
97
+ }
98
+ }
99
+
100
+ private async fetchRemoteConfig(serviceName: string, environment: string, grpcUrl: string, version: string): Promise<void> {
101
+ let credentials = grpc.credentials.createInsecure();
102
+
103
+ if (this.options.tls) {
104
+ credentials = grpc.credentials.createSsl(
105
+ this.options.tls.rootCert,
106
+ this.options.tls.clientKey,
107
+ this.options.tls.clientCert,
108
+ );
109
+ }
110
+
111
+ const client = new ConfigServiceClient(grpcUrl, credentials);
112
+
113
+ return new Promise((resolve) => {
114
+ const request: GetConfigRequest = { serviceName, environment, version };
115
+ const metadata = new grpc.Metadata();
116
+ if (this.options.internalKey) {
117
+ metadata.set('x-internal-key', this.options.internalKey);
118
+ }
119
+
120
+ client.getConfig(request, metadata, (error, response) => {
121
+ if (error) {
122
+ console.warn(`[ConfigSDK] Failed to fetch remote config from gRPC: ${error.message}. Using local/Vault fallback.`);
123
+ return resolve();
124
+ }
125
+
126
+ if (response && response.values) {
127
+ Object.assign(this.configMap, response.values);
128
+ console.log(`[ConfigSDK] Remote config loaded for ${serviceName} (${environment}, ${version})`);
129
+ }
130
+ resolve();
131
+ });
132
+ });
133
+ }
134
+
135
+ private async startWatching(): Promise<void> {
136
+ if (this.consumer) return;
137
+
138
+ try {
139
+ const kafka = new Kafka({
140
+ clientId: `config-sdk-${this.options.serviceName}`,
141
+ brokers: this.options.kafkaBrokers!,
142
+ });
143
+
144
+ const groupId = `config-watch-${this.options.serviceName}-${this.options.environment}`;
145
+
146
+ this.consumer = kafka.consumer({ groupId });
147
+ await this.consumer.connect();
148
+ await this.consumer.subscribe({ topic: 'config.updated', fromBeginning: false });
149
+
150
+ await this.consumer.run({
151
+ eachMessage: async ({ message }) => {
152
+ if (!message.value) return;
153
+ const event = JSON.parse(message.value.toString());
154
+
155
+ if (event.service === 'global' || event.service === this.options.serviceName) {
156
+ if (event.environment === this.options.environment && event.version === (this.options.version || 'v1')) {
157
+ console.log(`[ConfigSDK] Hot-reload: ${event.key} updated`);
158
+ this.configMap[event.key] = event.value;
159
+ }
160
+ }
161
+ },
162
+ });
163
+ } catch (error: any) {
164
+ console.error(`[ConfigSDK] Watch mode failed: ${error.message}`);
165
+ }
166
+ }
167
+
168
+ get(key: string, defaultValue: string = ''): string {
169
+ return this.configMap[key] || defaultValue;
170
+ }
171
+
172
+ getInt(key: string, defaultValue: number = 0): number {
173
+ const val = this.get(key);
174
+ return val ? parseInt(val, 10) : defaultValue;
175
+ }
176
+
177
+ getAll(): Record<string, string> {
178
+ return { ...this.configMap };
179
+ }
180
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './config-client';