@fonderie/config 2.0.0 → 4.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.
package/dist/index.d.cts CHANGED
@@ -1,17 +1,22 @@
1
- import { IFonderieModule, IFonderieApp } from '@fonderie/core';
1
+ import { IFonderieModule, IFonderieApp, Middleware } from '@fonderie/core';
2
2
  import { IStoreAdapter } from '@fonderie/store';
3
- import { R as RemoteConfigManager, I as IRemoteConfigOptions } from './index-v2hLmm2m.cjs';
4
- export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, g as getConfig } from './index-v2hLmm2m.cjs';
5
- import { IConfigEntry } from './types.cjs';
3
+ export { VersionConflictError as ConfigConflictError } from '@fonderie/store';
4
+ import { R as RemoteConfigManager, I as IConfigOptions, a as ISecretEncryptor } from './index-CLaJRR69.cjs';
5
+ export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, b as createAesGcmEncryptor, g as getConfig, n as noopEncryptor } from './index-CLaJRR69.cjs';
6
+ import { IConfigEntry, IConfigRevision, ISecretEntry, ISecretRevision } from './types.cjs';
6
7
  export { IConfigSnapshot } from './types.cjs';
7
8
 
8
- declare class RemoteConfigModule implements IFonderieModule {
9
+ declare class ConfigModule implements IFonderieModule {
10
+ private store;
11
+ private options;
9
12
  readonly name = "@fonderie/config";
10
13
  readonly manager: RemoteConfigManager;
11
- constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
14
+ constructor(store: IStoreAdapter, options?: IConfigOptions);
12
15
  install(app: IFonderieApp): Promise<void>;
13
16
  }
14
17
 
18
+ declare function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): Array<[string, string, Middleware]>;
19
+
15
20
  declare function listConfigEntries(environment: string | null, store: IStoreAdapter): Promise<IConfigEntry[]>;
16
21
  declare function getConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<IConfigEntry | null>;
17
22
  declare function setConfigEntry(opts: {
@@ -20,7 +25,37 @@ declare function setConfigEntry(opts: {
20
25
  environment?: string;
21
26
  description?: string;
22
27
  active?: boolean;
28
+ ifVersion?: number;
29
+ actor?: string;
30
+ }, store: IStoreAdapter): Promise<IConfigEntry>;
31
+ declare function rollbackConfigEntry(opts: {
32
+ key: string;
33
+ environment?: string;
34
+ toVersion: number;
35
+ actor?: string;
23
36
  }, store: IStoreAdapter): Promise<IConfigEntry>;
37
+ declare function listConfigRevisions(key: string, environment: string, store: IStoreAdapter): Promise<IConfigRevision[]>;
24
38
  declare function deleteConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
25
39
 
26
- export { IConfigEntry, IRemoteConfigOptions, RemoteConfigManager, RemoteConfigModule, deleteConfigEntry, getConfigEntry, listConfigEntries, setConfigEntry };
40
+ declare function listSecrets(environment: string | null, store: IStoreAdapter): Promise<ISecretEntry[]>;
41
+ declare function getSecret(key: string, environment: string, store: IStoreAdapter): Promise<ISecretEntry | null>;
42
+ declare function revealSecret(key: string, environment: string, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<string | null>;
43
+ declare function setSecret(opts: {
44
+ key: string;
45
+ value: string;
46
+ environment?: string;
47
+ description?: string;
48
+ active?: boolean;
49
+ ifVersion?: number;
50
+ actor?: string;
51
+ }, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<ISecretEntry>;
52
+ declare function rollbackSecret(opts: {
53
+ key: string;
54
+ environment?: string;
55
+ toVersion: number;
56
+ actor?: string;
57
+ }, store: IStoreAdapter): Promise<ISecretEntry>;
58
+ declare function listSecretRevisions(key: string, environment: string, store: IStoreAdapter): Promise<ISecretRevision[]>;
59
+ declare function deleteSecret(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
60
+
61
+ export { ConfigModule, IConfigEntry, IConfigOptions, IConfigRevision, ISecretEncryptor, ISecretEntry, ISecretRevision, RemoteConfigManager, buildAdminRoutes, deleteConfigEntry, deleteSecret, getConfigEntry, getSecret, listConfigEntries, listConfigRevisions, listSecretRevisions, listSecrets, revealSecret, rollbackConfigEntry, rollbackSecret, setConfigEntry, setSecret };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { IFonderieModule, IFonderieApp } from '@fonderie/core';
1
+ import { IFonderieModule, IFonderieApp, Middleware } from '@fonderie/core';
2
2
  import { IStoreAdapter } from '@fonderie/store';
3
- import { R as RemoteConfigManager, I as IRemoteConfigOptions } from './index-v2hLmm2m.js';
4
- export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, g as getConfig } from './index-v2hLmm2m.js';
5
- import { IConfigEntry } from './types.js';
3
+ export { VersionConflictError as ConfigConflictError } from '@fonderie/store';
4
+ import { R as RemoteConfigManager, I as IConfigOptions, a as ISecretEncryptor } from './index-CLaJRR69.js';
5
+ export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, b as createAesGcmEncryptor, g as getConfig, n as noopEncryptor } from './index-CLaJRR69.js';
6
+ import { IConfigEntry, IConfigRevision, ISecretEntry, ISecretRevision } from './types.js';
6
7
  export { IConfigSnapshot } from './types.js';
7
8
 
8
- declare class RemoteConfigModule implements IFonderieModule {
9
+ declare class ConfigModule implements IFonderieModule {
10
+ private store;
11
+ private options;
9
12
  readonly name = "@fonderie/config";
10
13
  readonly manager: RemoteConfigManager;
11
- constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
14
+ constructor(store: IStoreAdapter, options?: IConfigOptions);
12
15
  install(app: IFonderieApp): Promise<void>;
13
16
  }
14
17
 
18
+ declare function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): Array<[string, string, Middleware]>;
19
+
15
20
  declare function listConfigEntries(environment: string | null, store: IStoreAdapter): Promise<IConfigEntry[]>;
16
21
  declare function getConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<IConfigEntry | null>;
17
22
  declare function setConfigEntry(opts: {
@@ -20,7 +25,37 @@ declare function setConfigEntry(opts: {
20
25
  environment?: string;
21
26
  description?: string;
22
27
  active?: boolean;
28
+ ifVersion?: number;
29
+ actor?: string;
30
+ }, store: IStoreAdapter): Promise<IConfigEntry>;
31
+ declare function rollbackConfigEntry(opts: {
32
+ key: string;
33
+ environment?: string;
34
+ toVersion: number;
35
+ actor?: string;
23
36
  }, store: IStoreAdapter): Promise<IConfigEntry>;
37
+ declare function listConfigRevisions(key: string, environment: string, store: IStoreAdapter): Promise<IConfigRevision[]>;
24
38
  declare function deleteConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
25
39
 
26
- export { IConfigEntry, IRemoteConfigOptions, RemoteConfigManager, RemoteConfigModule, deleteConfigEntry, getConfigEntry, listConfigEntries, setConfigEntry };
40
+ declare function listSecrets(environment: string | null, store: IStoreAdapter): Promise<ISecretEntry[]>;
41
+ declare function getSecret(key: string, environment: string, store: IStoreAdapter): Promise<ISecretEntry | null>;
42
+ declare function revealSecret(key: string, environment: string, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<string | null>;
43
+ declare function setSecret(opts: {
44
+ key: string;
45
+ value: string;
46
+ environment?: string;
47
+ description?: string;
48
+ active?: boolean;
49
+ ifVersion?: number;
50
+ actor?: string;
51
+ }, store: IStoreAdapter, encryptor?: ISecretEncryptor): Promise<ISecretEntry>;
52
+ declare function rollbackSecret(opts: {
53
+ key: string;
54
+ environment?: string;
55
+ toVersion: number;
56
+ actor?: string;
57
+ }, store: IStoreAdapter): Promise<ISecretEntry>;
58
+ declare function listSecretRevisions(key: string, environment: string, store: IStoreAdapter): Promise<ISecretRevision[]>;
59
+ declare function deleteSecret(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
60
+
61
+ export { ConfigModule, IConfigEntry, IConfigOptions, IConfigRevision, ISecretEncryptor, ISecretEntry, ISecretRevision, RemoteConfigManager, buildAdminRoutes, deleteConfigEntry, deleteSecret, getConfigEntry, getSecret, listConfigEntries, listConfigRevisions, listSecretRevisions, listSecrets, revealSecret, rollbackConfigEntry, rollbackSecret, setConfigEntry, setSecret };
package/dist/index.js CHANGED
@@ -1,29 +1,54 @@
1
1
  // src/manager.ts
2
+ import pg from "pg";
2
3
  var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
4
+ var NOTIFY_CHANNEL = "fonderie_config_changed";
3
5
  var RemoteConfigManager = class {
4
6
  constructor(store, options = {}) {
5
7
  this.store = store;
6
8
  this.ttl = options.ttl ?? 3e4;
7
9
  this.environment = options.environment ?? process.env["NODE_ENV"] ?? "development";
8
10
  this.table = options.table ?? "fonderie_config";
11
+ this.connectionUrl = options.connectionUrl;
9
12
  }
10
13
  store;
11
14
  snapshot = null;
12
15
  interval = null;
16
+ listenClient = null;
13
17
  environment;
14
18
  ttl;
15
19
  table;
20
+ connectionUrl;
16
21
  async boot() {
17
22
  await this.refresh();
18
23
  this.interval = setInterval(() => {
19
24
  this.refresh().catch((err) => console.error("[config] refresh error:", err));
20
25
  }, this.ttl);
26
+ if (this.connectionUrl) await this.startListening();
27
+ }
28
+ async startListening() {
29
+ try {
30
+ this.listenClient = new pg.Client(this.connectionUrl);
31
+ await this.listenClient.connect();
32
+ await this.listenClient.query(`LISTEN ${NOTIFY_CHANNEL}`);
33
+ this.listenClient.on("notification", () => {
34
+ this.refresh().catch((err) => console.error("[config] refresh error:", err));
35
+ });
36
+ this.listenClient.on(
37
+ "error",
38
+ (err) => console.error("[config] listen client error:", err.message)
39
+ );
40
+ } catch (err) {
41
+ console.error("[config] failed to start LISTEN, falling back to poll:", err);
42
+ this.listenClient = null;
43
+ }
21
44
  }
22
45
  stop() {
23
46
  if (this.interval) {
24
47
  clearInterval(this.interval);
25
48
  this.interval = null;
26
49
  }
50
+ void this.listenClient?.end();
51
+ this.listenClient = null;
27
52
  }
28
53
  // Get a single value with a typed fallback
29
54
  get(key, fallback) {
@@ -97,29 +122,71 @@ function getConfig(ctx, key, fallback = null) {
97
122
  return manager.get(key, fallback);
98
123
  }
99
124
 
100
- // src/module.ts
101
- var RemoteConfigModule = class {
102
- name = "@fonderie/config";
103
- manager;
104
- constructor(store, options = {}) {
105
- this.manager = new RemoteConfigManager(store, options);
106
- }
107
- async install(app) {
108
- await this.manager.boot();
109
- app.use(configContextMiddleware(this.manager));
110
- }
125
+ // src/admin.ts
126
+ import { setApiResponse, HTTP } from "@fonderie/core";
127
+
128
+ // src/crypto.ts
129
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
130
+ var noopEncryptor = {
131
+ encrypt: (plain) => plain,
132
+ decrypt: (cipher) => cipher
111
133
  };
134
+ function createAesGcmEncryptor(keyHex) {
135
+ const key = Buffer.from(keyHex, "hex");
136
+ if (key.length !== 32) {
137
+ throw new Error("[config] secret encryptor key must be 32 bytes (64 hex chars)");
138
+ }
139
+ return {
140
+ encrypt(plain) {
141
+ const iv = randomBytes(12);
142
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
143
+ const enc = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
144
+ const tag = cipher.getAuthTag();
145
+ return `${iv.toString("hex")}:${tag.toString("hex")}:${enc.toString("hex")}`;
146
+ },
147
+ decrypt(cipher) {
148
+ const [ivHex, tagHex, dataHex] = cipher.split(":");
149
+ if (!ivHex || !tagHex || !dataHex) {
150
+ throw new Error("[config] malformed secret ciphertext");
151
+ }
152
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
153
+ decipher.setAuthTag(Buffer.from(tagHex, "hex"));
154
+ return Buffer.concat([
155
+ decipher.update(Buffer.from(dataHex, "hex")),
156
+ decipher.final()
157
+ ]).toString("utf8");
158
+ }
159
+ };
160
+ }
161
+
162
+ // src/services/versioned.ts
163
+ import {
164
+ versionedWrite,
165
+ versionedRollback,
166
+ VersionConflictError,
167
+ VersionConflictError as VersionConflictError2
168
+ } from "@fonderie/store";
112
169
 
113
170
  // src/services/config.ts
114
- var SELECT_ENTRY = `
115
- SELECT
116
- key,
117
- value,
118
- environment,
119
- description,
120
- active,
121
- updated_at AS "updatedAt"
122
- FROM fonderie_config`;
171
+ var ENTRY_COLS = `
172
+ key,
173
+ value,
174
+ environment,
175
+ description,
176
+ active,
177
+ version,
178
+ updated_by AS "updatedBy",
179
+ updated_at AS "updatedAt"`;
180
+ var SELECT_ENTRY = `SELECT ${ENTRY_COLS} FROM fonderie_config`;
181
+ var CONFIG_TABLE = {
182
+ table: "fonderie_config",
183
+ revisions: "fonderie_config_revisions",
184
+ channel: "fonderie_config_changed",
185
+ keyColumns: ["key", "environment"],
186
+ contentColumns: ["value"],
187
+ metaColumns: ["description", "active"],
188
+ returning: ENTRY_COLS
189
+ };
123
190
  async function listConfigEntries(environment, store) {
124
191
  return environment ? store.query(
125
192
  `${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
@@ -134,24 +201,33 @@ async function getConfigEntry(key, environment, store) {
134
201
  return row ?? null;
135
202
  }
136
203
  async function setConfigEntry(opts, store) {
137
- const env = opts.environment ?? "all";
138
- const raw = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
139
- const active = opts.active ?? true;
140
- const [row] = await store.query(
141
- `INSERT INTO fonderie_config (key, value, environment, description, active)
142
- VALUES ($1, $2, $3, $4, $5)
143
- ON CONFLICT (key, environment) DO UPDATE SET
144
- value = $2,
145
- description = COALESCE($4, fonderie_config.description),
146
- active = $5,
147
- updated_at = now()
148
- RETURNING
149
- key, value, environment, description, active,
150
- updated_at AS "updatedAt"`,
151
- [opts.key, raw, env, opts.description ?? null, active]
204
+ const rawValue = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
205
+ const data = { value: rawValue, active: opts.active ?? true };
206
+ if (opts.description !== void 0) data["description"] = opts.description;
207
+ return versionedWrite(CONFIG_TABLE, store, {
208
+ key: opts.key,
209
+ scope: opts.environment ?? "all",
210
+ data,
211
+ ...opts.ifVersion !== void 0 ? { ifVersion: opts.ifVersion } : {},
212
+ actor: opts.actor ?? null
213
+ });
214
+ }
215
+ async function rollbackConfigEntry(opts, store) {
216
+ return versionedRollback(CONFIG_TABLE, store, {
217
+ key: opts.key,
218
+ scope: opts.environment ?? "all",
219
+ toVersion: opts.toVersion,
220
+ actor: opts.actor ?? null
221
+ });
222
+ }
223
+ async function listConfigRevisions(key, environment, store) {
224
+ return store.query(
225
+ `SELECT key, environment, value, version, actor, created_at AS "createdAt"
226
+ FROM fonderie_config_revisions
227
+ WHERE key = $1 AND environment = $2
228
+ ORDER BY version DESC`,
229
+ [key, environment]
152
230
  );
153
- if (!row) throw new Error("Failed to upsert config entry");
154
- return row;
155
231
  }
156
232
  async function deleteConfigEntry(key, environment, store) {
157
233
  const rows = await store.query(
@@ -160,15 +236,292 @@ async function deleteConfigEntry(key, environment, store) {
160
236
  );
161
237
  return rows.length > 0;
162
238
  }
239
+
240
+ // src/services/secrets.ts
241
+ var META_COLS = `
242
+ key,
243
+ environment,
244
+ description,
245
+ active,
246
+ version,
247
+ updated_by AS "updatedBy",
248
+ updated_at AS "updatedAt"`;
249
+ var SECRET_TABLE = {
250
+ table: "fonderie_secrets",
251
+ revisions: "fonderie_secret_revisions",
252
+ channel: "fonderie_secrets_changed",
253
+ keyColumns: ["key", "environment"],
254
+ contentColumns: ["value"],
255
+ metaColumns: ["description", "active"],
256
+ returning: META_COLS
257
+ // masked — no value
258
+ };
259
+ async function listSecrets(environment, store) {
260
+ return environment ? store.query(
261
+ `SELECT ${META_COLS} FROM fonderie_secrets
262
+ WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
263
+ [environment]
264
+ ) : store.query(
265
+ `SELECT ${META_COLS} FROM fonderie_secrets ORDER BY environment, key`
266
+ );
267
+ }
268
+ async function getSecret(key, environment, store) {
269
+ const [row] = await store.query(
270
+ `SELECT ${META_COLS} FROM fonderie_secrets WHERE key = $1 AND environment = $2`,
271
+ [key, environment]
272
+ );
273
+ return row ?? null;
274
+ }
275
+ async function revealSecret(key, environment, store, encryptor = noopEncryptor) {
276
+ const [row] = await store.query(
277
+ `SELECT value FROM fonderie_secrets WHERE key = $1 AND environment = $2`,
278
+ [key, environment]
279
+ );
280
+ return row ? encryptor.decrypt(row.value) : null;
281
+ }
282
+ async function setSecret(opts, store, encryptor = noopEncryptor) {
283
+ const data = {
284
+ value: encryptor.encrypt(opts.value),
285
+ active: opts.active ?? true
286
+ };
287
+ if (opts.description !== void 0) data["description"] = opts.description;
288
+ return versionedWrite(SECRET_TABLE, store, {
289
+ key: opts.key,
290
+ scope: opts.environment ?? "all",
291
+ data,
292
+ ...opts.ifVersion !== void 0 ? { ifVersion: opts.ifVersion } : {},
293
+ actor: opts.actor ?? null
294
+ });
295
+ }
296
+ async function rollbackSecret(opts, store) {
297
+ return versionedRollback(SECRET_TABLE, store, {
298
+ key: opts.key,
299
+ scope: opts.environment ?? "all",
300
+ toVersion: opts.toVersion,
301
+ actor: opts.actor ?? null
302
+ });
303
+ }
304
+ async function listSecretRevisions(key, environment, store) {
305
+ return store.query(
306
+ `SELECT key, environment, version, actor, created_at AS "createdAt"
307
+ FROM fonderie_secret_revisions
308
+ WHERE key = $1 AND environment = $2
309
+ ORDER BY version DESC`,
310
+ [key, environment]
311
+ );
312
+ }
313
+ async function deleteSecret(key, environment, store) {
314
+ const rows = await store.query(
315
+ `DELETE FROM fonderie_secrets WHERE key = $1 AND environment = $2 RETURNING key`,
316
+ [key, environment]
317
+ );
318
+ return rows.length > 0;
319
+ }
320
+
321
+ // src/admin.ts
322
+ function checkAdmin(ctx, adminToken) {
323
+ const header = ctx.request.headers.get("authorization") ?? "";
324
+ const token = header.startsWith("Bearer ") ? header.slice(7) : "";
325
+ if (!token || token !== adminToken) {
326
+ return setApiResponse(HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
327
+ }
328
+ return null;
329
+ }
330
+ function actorOf(ctx) {
331
+ return ctx.request.headers.get("x-actor") || "admin-token";
332
+ }
333
+ function envOf(ctx) {
334
+ return new URL(ctx.request.url).searchParams.get("environment") ?? void 0;
335
+ }
336
+ function keyOf(ctx) {
337
+ return ctx.meta.params?.["key"] ?? "";
338
+ }
339
+ function body(ctx) {
340
+ return ctx.meta["body"] ?? {};
341
+ }
342
+ function writeOpts(ctx, b) {
343
+ const opts = {
344
+ actor: actorOf(ctx)
345
+ };
346
+ const env = b["environment"] ?? envOf(ctx);
347
+ if (env !== void 0) opts.environment = env;
348
+ if (typeof b["description"] === "string") opts.description = b["description"];
349
+ if (typeof b["ifVersion"] === "number") opts.ifVersion = b["ifVersion"];
350
+ return opts;
351
+ }
352
+ function rollbackOpts(ctx, b, toVersion) {
353
+ const opts = {
354
+ key: keyOf(ctx),
355
+ toVersion,
356
+ actor: actorOf(ctx)
357
+ };
358
+ const env = b["environment"] ?? envOf(ctx);
359
+ if (env !== void 0) opts.environment = env;
360
+ return opts;
361
+ }
362
+ function guarded(adminToken, handler) {
363
+ return async (ctx, next) => {
364
+ const denied = checkAdmin(ctx, adminToken);
365
+ return denied ?? handler(ctx, next);
366
+ };
367
+ }
368
+ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
369
+ const g = (h) => guarded(adminToken, h);
370
+ return [
371
+ // ── config ──────────────────────────────────────────────────
372
+ ["GET", "/admin/config", g(async (ctx) => {
373
+ const rows = await listConfigEntries(envOf(ctx) ?? null, store);
374
+ return setApiResponse(HTTP.OK, "CONFIG_LISTED", "Config entries", rows);
375
+ })],
376
+ ["GET", "/admin/config/:key", g(async (ctx) => {
377
+ const row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? "all", store);
378
+ return row ? setApiResponse(HTTP.OK, "CONFIG_ENTRY", "Config entry", row) : setApiResponse(HTTP.NOT_FOUND, "NOT_FOUND", "No such config entry");
379
+ })],
380
+ ["PUT", "/admin/config/:key", g(async (ctx) => {
381
+ const b = body(ctx);
382
+ if (!("value" in b)) {
383
+ return setApiResponse(HTTP.UNPROCESSABLE, "INVALID", "body.value is required");
384
+ }
385
+ try {
386
+ const row = await setConfigEntry(
387
+ { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
388
+ store
389
+ );
390
+ return setApiResponse(HTTP.OK, "CONFIG_SET", "Config entry saved", row);
391
+ } catch (err) {
392
+ return conflictOr(err);
393
+ }
394
+ })],
395
+ ["DELETE", "/admin/config/:key", g(async (ctx) => {
396
+ const ok = await deleteConfigEntry(keyOf(ctx), envOf(ctx) ?? "all", store);
397
+ return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? "DELETED" : "NOT_FOUND", ok ? "Deleted" : "No such config entry");
398
+ })],
399
+ ["GET", "/admin/config/:key/revisions", g(async (ctx) => {
400
+ const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
401
+ return setApiResponse(HTTP.OK, "REVISIONS", "Config revisions", revs);
402
+ })],
403
+ ["POST", "/admin/config/:key/rollback", g(async (ctx) => {
404
+ const b = body(ctx);
405
+ const toVersion = Number(b["toVersion"]);
406
+ if (!Number.isInteger(toVersion)) {
407
+ return setApiResponse(HTTP.UNPROCESSABLE, "INVALID", "body.toVersion (int) is required");
408
+ }
409
+ const row = await rollbackConfigEntry(
410
+ rollbackOpts(ctx, b, toVersion),
411
+ store
412
+ );
413
+ return setApiResponse(HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
414
+ })],
415
+ // ── secrets (masked) ────────────────────────────────────────
416
+ ["GET", "/admin/secrets", g(async (ctx) => {
417
+ const rows = await listSecrets(envOf(ctx) ?? null, store);
418
+ return setApiResponse(HTTP.OK, "SECRETS_LISTED", "Secrets (masked)", rows);
419
+ })],
420
+ ["GET", "/admin/secrets/:key", g(async (ctx) => {
421
+ const row = await getSecret(keyOf(ctx), envOf(ctx) ?? "all", store);
422
+ return row ? setApiResponse(HTTP.OK, "SECRET", "Secret (masked)", row) : setApiResponse(HTTP.NOT_FOUND, "NOT_FOUND", "No such secret");
423
+ })],
424
+ ["PUT", "/admin/secrets/:key", g(async (ctx) => {
425
+ const b = body(ctx);
426
+ if (typeof b["value"] !== "string") {
427
+ return setApiResponse(HTTP.UNPROCESSABLE, "INVALID", "body.value (string) is required");
428
+ }
429
+ try {
430
+ const row = await setSecret(
431
+ { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
432
+ store,
433
+ encryptor
434
+ );
435
+ return setApiResponse(HTTP.OK, "SECRET_SET", "Secret saved (masked)", row);
436
+ } catch (err) {
437
+ return conflictOr(err);
438
+ }
439
+ })],
440
+ ["DELETE", "/admin/secrets/:key", g(async (ctx) => {
441
+ const ok = await deleteSecret(keyOf(ctx), envOf(ctx) ?? "all", store);
442
+ return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? "DELETED" : "NOT_FOUND", ok ? "Deleted" : "No such secret");
443
+ })],
444
+ ["GET", "/admin/secrets/:key/revisions", g(async (ctx) => {
445
+ const revs = await listSecretRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
446
+ return setApiResponse(HTTP.OK, "REVISIONS", "Secret revisions", revs);
447
+ })],
448
+ ["POST", "/admin/secrets/:key/rollback", g(async (ctx) => {
449
+ const b = body(ctx);
450
+ const toVersion = Number(b["toVersion"]);
451
+ if (!Number.isInteger(toVersion)) {
452
+ return setApiResponse(HTTP.UNPROCESSABLE, "INVALID", "body.toVersion (int) is required");
453
+ }
454
+ const row = await rollbackSecret(
455
+ rollbackOpts(ctx, b, toVersion),
456
+ store
457
+ );
458
+ return setApiResponse(HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
459
+ })],
460
+ // The one plaintext path — behind the same token; POST so the value never
461
+ // lands in a URL/log. Returns the decrypted value.
462
+ ["POST", "/admin/secrets/:key/reveal", g(async (ctx) => {
463
+ const value = await revealSecret(keyOf(ctx), envOf(ctx) ?? "all", store, encryptor);
464
+ return value === null ? setApiResponse(HTTP.NOT_FOUND, "NOT_FOUND", "No such secret") : setApiResponse(HTTP.OK, "SECRET_REVEALED", "Decrypted secret value", { value });
465
+ })]
466
+ ];
467
+ }
468
+ function conflictOr(err) {
469
+ if (err instanceof VersionConflictError2) {
470
+ return setApiResponse(HTTP.CONFLICT, "VERSION_CONFLICT", err.message, {
471
+ currentVersion: err.currentVersion
472
+ });
473
+ }
474
+ throw err;
475
+ }
476
+
477
+ // src/module.ts
478
+ var ConfigModule = class {
479
+ constructor(store, options = {}) {
480
+ this.store = store;
481
+ this.options = options;
482
+ this.manager = new RemoteConfigManager(store, options);
483
+ }
484
+ store;
485
+ options;
486
+ name = "@fonderie/config";
487
+ manager;
488
+ async install(app) {
489
+ await this.manager.boot();
490
+ app.use(configContextMiddleware(this.manager));
491
+ if (this.options.adminToken) {
492
+ const routes = buildAdminRoutes(
493
+ this.store,
494
+ this.options.adminToken,
495
+ this.options.secretEncryptor ?? noopEncryptor
496
+ );
497
+ for (const [method, path, handler] of routes) {
498
+ app.addRoute(method, path, handler);
499
+ }
500
+ }
501
+ }
502
+ };
163
503
  export {
164
504
  CONFIG_MANAGER_KEY,
505
+ VersionConflictError2 as ConfigConflictError,
506
+ ConfigModule,
165
507
  RemoteConfigManager,
166
- RemoteConfigModule,
508
+ buildAdminRoutes,
167
509
  configContextMiddleware,
510
+ createAesGcmEncryptor,
168
511
  deleteConfigEntry,
512
+ deleteSecret,
169
513
  getConfig,
170
514
  getConfigEntry,
515
+ getSecret,
171
516
  listConfigEntries,
172
- setConfigEntry
517
+ listConfigRevisions,
518
+ listSecretRevisions,
519
+ listSecrets,
520
+ noopEncryptor,
521
+ revealSecret,
522
+ rollbackConfigEntry,
523
+ rollbackSecret,
524
+ setConfigEntry,
525
+ setSecret
173
526
  };
174
527
  //# sourceMappingURL=index.js.map