@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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,49 +17,95 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
33
  CONFIG_MANAGER_KEY: () => CONFIG_MANAGER_KEY,
34
+ ConfigConflictError: () => import_store.VersionConflictError,
35
+ ConfigModule: () => ConfigModule,
24
36
  RemoteConfigManager: () => RemoteConfigManager,
25
- RemoteConfigModule: () => RemoteConfigModule,
37
+ buildAdminRoutes: () => buildAdminRoutes,
26
38
  configContextMiddleware: () => configContextMiddleware,
39
+ createAesGcmEncryptor: () => createAesGcmEncryptor,
27
40
  deleteConfigEntry: () => deleteConfigEntry,
41
+ deleteSecret: () => deleteSecret,
28
42
  getConfig: () => getConfig,
29
43
  getConfigEntry: () => getConfigEntry,
44
+ getSecret: () => getSecret,
30
45
  listConfigEntries: () => listConfigEntries,
31
- setConfigEntry: () => setConfigEntry
46
+ listConfigRevisions: () => listConfigRevisions,
47
+ listSecretRevisions: () => listSecretRevisions,
48
+ listSecrets: () => listSecrets,
49
+ noopEncryptor: () => noopEncryptor,
50
+ revealSecret: () => revealSecret,
51
+ rollbackConfigEntry: () => rollbackConfigEntry,
52
+ rollbackSecret: () => rollbackSecret,
53
+ setConfigEntry: () => setConfigEntry,
54
+ setSecret: () => setSecret
32
55
  });
33
56
  module.exports = __toCommonJS(index_exports);
34
57
 
35
58
  // src/manager.ts
59
+ var import_pg = __toESM(require("pg"), 1);
36
60
  var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
61
+ var NOTIFY_CHANNEL = "fonderie_config_changed";
37
62
  var RemoteConfigManager = class {
38
63
  constructor(store, options = {}) {
39
64
  this.store = store;
40
65
  this.ttl = options.ttl ?? 3e4;
41
66
  this.environment = options.environment ?? process.env["NODE_ENV"] ?? "development";
42
67
  this.table = options.table ?? "fonderie_config";
68
+ this.connectionUrl = options.connectionUrl;
43
69
  }
44
70
  store;
45
71
  snapshot = null;
46
72
  interval = null;
73
+ listenClient = null;
47
74
  environment;
48
75
  ttl;
49
76
  table;
77
+ connectionUrl;
50
78
  async boot() {
51
79
  await this.refresh();
52
80
  this.interval = setInterval(() => {
53
81
  this.refresh().catch((err) => console.error("[config] refresh error:", err));
54
82
  }, this.ttl);
83
+ if (this.connectionUrl) await this.startListening();
84
+ }
85
+ async startListening() {
86
+ try {
87
+ this.listenClient = new import_pg.default.Client(this.connectionUrl);
88
+ await this.listenClient.connect();
89
+ await this.listenClient.query(`LISTEN ${NOTIFY_CHANNEL}`);
90
+ this.listenClient.on("notification", () => {
91
+ this.refresh().catch((err) => console.error("[config] refresh error:", err));
92
+ });
93
+ this.listenClient.on(
94
+ "error",
95
+ (err) => console.error("[config] listen client error:", err.message)
96
+ );
97
+ } catch (err) {
98
+ console.error("[config] failed to start LISTEN, falling back to poll:", err);
99
+ this.listenClient = null;
100
+ }
55
101
  }
56
102
  stop() {
57
103
  if (this.interval) {
58
104
  clearInterval(this.interval);
59
105
  this.interval = null;
60
106
  }
107
+ void this.listenClient?.end();
108
+ this.listenClient = null;
61
109
  }
62
110
  // Get a single value with a typed fallback
63
111
  get(key, fallback) {
@@ -131,29 +179,66 @@ function getConfig(ctx, key, fallback = null) {
131
179
  return manager.get(key, fallback);
132
180
  }
133
181
 
134
- // src/module.ts
135
- var RemoteConfigModule = class {
136
- name = "@fonderie/config";
137
- manager;
138
- constructor(store, options = {}) {
139
- this.manager = new RemoteConfigManager(store, options);
140
- }
141
- async install(app) {
142
- await this.manager.boot();
143
- app.use(configContextMiddleware(this.manager));
144
- }
182
+ // src/admin.ts
183
+ var import_core = require("@fonderie/core");
184
+
185
+ // src/crypto.ts
186
+ var import_node_crypto = require("crypto");
187
+ var noopEncryptor = {
188
+ encrypt: (plain) => plain,
189
+ decrypt: (cipher) => cipher
145
190
  };
191
+ function createAesGcmEncryptor(keyHex) {
192
+ const key = Buffer.from(keyHex, "hex");
193
+ if (key.length !== 32) {
194
+ throw new Error("[config] secret encryptor key must be 32 bytes (64 hex chars)");
195
+ }
196
+ return {
197
+ encrypt(plain) {
198
+ const iv = (0, import_node_crypto.randomBytes)(12);
199
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", key, iv);
200
+ const enc = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
201
+ const tag = cipher.getAuthTag();
202
+ return `${iv.toString("hex")}:${tag.toString("hex")}:${enc.toString("hex")}`;
203
+ },
204
+ decrypt(cipher) {
205
+ const [ivHex, tagHex, dataHex] = cipher.split(":");
206
+ if (!ivHex || !tagHex || !dataHex) {
207
+ throw new Error("[config] malformed secret ciphertext");
208
+ }
209
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
210
+ decipher.setAuthTag(Buffer.from(tagHex, "hex"));
211
+ return Buffer.concat([
212
+ decipher.update(Buffer.from(dataHex, "hex")),
213
+ decipher.final()
214
+ ]).toString("utf8");
215
+ }
216
+ };
217
+ }
218
+
219
+ // src/services/versioned.ts
220
+ var import_store = require("@fonderie/store");
146
221
 
147
222
  // src/services/config.ts
148
- var SELECT_ENTRY = `
149
- SELECT
150
- key,
151
- value,
152
- environment,
153
- description,
154
- active,
155
- updated_at AS "updatedAt"
156
- FROM fonderie_config`;
223
+ var ENTRY_COLS = `
224
+ key,
225
+ value,
226
+ environment,
227
+ description,
228
+ active,
229
+ version,
230
+ updated_by AS "updatedBy",
231
+ updated_at AS "updatedAt"`;
232
+ var SELECT_ENTRY = `SELECT ${ENTRY_COLS} FROM fonderie_config`;
233
+ var CONFIG_TABLE = {
234
+ table: "fonderie_config",
235
+ revisions: "fonderie_config_revisions",
236
+ channel: "fonderie_config_changed",
237
+ keyColumns: ["key", "environment"],
238
+ contentColumns: ["value"],
239
+ metaColumns: ["description", "active"],
240
+ returning: ENTRY_COLS
241
+ };
157
242
  async function listConfigEntries(environment, store) {
158
243
  return environment ? store.query(
159
244
  `${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
@@ -168,24 +253,33 @@ async function getConfigEntry(key, environment, store) {
168
253
  return row ?? null;
169
254
  }
170
255
  async function setConfigEntry(opts, store) {
171
- const env = opts.environment ?? "all";
172
- const raw = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
173
- const active = opts.active ?? true;
174
- const [row] = await store.query(
175
- `INSERT INTO fonderie_config (key, value, environment, description, active)
176
- VALUES ($1, $2, $3, $4, $5)
177
- ON CONFLICT (key, environment) DO UPDATE SET
178
- value = $2,
179
- description = COALESCE($4, fonderie_config.description),
180
- active = $5,
181
- updated_at = now()
182
- RETURNING
183
- key, value, environment, description, active,
184
- updated_at AS "updatedAt"`,
185
- [opts.key, raw, env, opts.description ?? null, active]
256
+ const rawValue = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
257
+ const data = { value: rawValue, active: opts.active ?? true };
258
+ if (opts.description !== void 0) data["description"] = opts.description;
259
+ return (0, import_store.versionedWrite)(CONFIG_TABLE, store, {
260
+ key: opts.key,
261
+ scope: opts.environment ?? "all",
262
+ data,
263
+ ...opts.ifVersion !== void 0 ? { ifVersion: opts.ifVersion } : {},
264
+ actor: opts.actor ?? null
265
+ });
266
+ }
267
+ async function rollbackConfigEntry(opts, store) {
268
+ return (0, import_store.versionedRollback)(CONFIG_TABLE, store, {
269
+ key: opts.key,
270
+ scope: opts.environment ?? "all",
271
+ toVersion: opts.toVersion,
272
+ actor: opts.actor ?? null
273
+ });
274
+ }
275
+ async function listConfigRevisions(key, environment, store) {
276
+ return store.query(
277
+ `SELECT key, environment, value, version, actor, created_at AS "createdAt"
278
+ FROM fonderie_config_revisions
279
+ WHERE key = $1 AND environment = $2
280
+ ORDER BY version DESC`,
281
+ [key, environment]
186
282
  );
187
- if (!row) throw new Error("Failed to upsert config entry");
188
- return row;
189
283
  }
190
284
  async function deleteConfigEntry(key, environment, store) {
191
285
  const rows = await store.query(
@@ -194,16 +288,293 @@ async function deleteConfigEntry(key, environment, store) {
194
288
  );
195
289
  return rows.length > 0;
196
290
  }
291
+
292
+ // src/services/secrets.ts
293
+ var META_COLS = `
294
+ key,
295
+ environment,
296
+ description,
297
+ active,
298
+ version,
299
+ updated_by AS "updatedBy",
300
+ updated_at AS "updatedAt"`;
301
+ var SECRET_TABLE = {
302
+ table: "fonderie_secrets",
303
+ revisions: "fonderie_secret_revisions",
304
+ channel: "fonderie_secrets_changed",
305
+ keyColumns: ["key", "environment"],
306
+ contentColumns: ["value"],
307
+ metaColumns: ["description", "active"],
308
+ returning: META_COLS
309
+ // masked — no value
310
+ };
311
+ async function listSecrets(environment, store) {
312
+ return environment ? store.query(
313
+ `SELECT ${META_COLS} FROM fonderie_secrets
314
+ WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
315
+ [environment]
316
+ ) : store.query(
317
+ `SELECT ${META_COLS} FROM fonderie_secrets ORDER BY environment, key`
318
+ );
319
+ }
320
+ async function getSecret(key, environment, store) {
321
+ const [row] = await store.query(
322
+ `SELECT ${META_COLS} FROM fonderie_secrets WHERE key = $1 AND environment = $2`,
323
+ [key, environment]
324
+ );
325
+ return row ?? null;
326
+ }
327
+ async function revealSecret(key, environment, store, encryptor = noopEncryptor) {
328
+ const [row] = await store.query(
329
+ `SELECT value FROM fonderie_secrets WHERE key = $1 AND environment = $2`,
330
+ [key, environment]
331
+ );
332
+ return row ? encryptor.decrypt(row.value) : null;
333
+ }
334
+ async function setSecret(opts, store, encryptor = noopEncryptor) {
335
+ const data = {
336
+ value: encryptor.encrypt(opts.value),
337
+ active: opts.active ?? true
338
+ };
339
+ if (opts.description !== void 0) data["description"] = opts.description;
340
+ return (0, import_store.versionedWrite)(SECRET_TABLE, store, {
341
+ key: opts.key,
342
+ scope: opts.environment ?? "all",
343
+ data,
344
+ ...opts.ifVersion !== void 0 ? { ifVersion: opts.ifVersion } : {},
345
+ actor: opts.actor ?? null
346
+ });
347
+ }
348
+ async function rollbackSecret(opts, store) {
349
+ return (0, import_store.versionedRollback)(SECRET_TABLE, store, {
350
+ key: opts.key,
351
+ scope: opts.environment ?? "all",
352
+ toVersion: opts.toVersion,
353
+ actor: opts.actor ?? null
354
+ });
355
+ }
356
+ async function listSecretRevisions(key, environment, store) {
357
+ return store.query(
358
+ `SELECT key, environment, version, actor, created_at AS "createdAt"
359
+ FROM fonderie_secret_revisions
360
+ WHERE key = $1 AND environment = $2
361
+ ORDER BY version DESC`,
362
+ [key, environment]
363
+ );
364
+ }
365
+ async function deleteSecret(key, environment, store) {
366
+ const rows = await store.query(
367
+ `DELETE FROM fonderie_secrets WHERE key = $1 AND environment = $2 RETURNING key`,
368
+ [key, environment]
369
+ );
370
+ return rows.length > 0;
371
+ }
372
+
373
+ // src/admin.ts
374
+ function checkAdmin(ctx, adminToken) {
375
+ const header = ctx.request.headers.get("authorization") ?? "";
376
+ const token = header.startsWith("Bearer ") ? header.slice(7) : "";
377
+ if (!token || token !== adminToken) {
378
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
379
+ }
380
+ return null;
381
+ }
382
+ function actorOf(ctx) {
383
+ return ctx.request.headers.get("x-actor") || "admin-token";
384
+ }
385
+ function envOf(ctx) {
386
+ return new URL(ctx.request.url).searchParams.get("environment") ?? void 0;
387
+ }
388
+ function keyOf(ctx) {
389
+ return ctx.meta.params?.["key"] ?? "";
390
+ }
391
+ function body(ctx) {
392
+ return ctx.meta["body"] ?? {};
393
+ }
394
+ function writeOpts(ctx, b) {
395
+ const opts = {
396
+ actor: actorOf(ctx)
397
+ };
398
+ const env = b["environment"] ?? envOf(ctx);
399
+ if (env !== void 0) opts.environment = env;
400
+ if (typeof b["description"] === "string") opts.description = b["description"];
401
+ if (typeof b["ifVersion"] === "number") opts.ifVersion = b["ifVersion"];
402
+ return opts;
403
+ }
404
+ function rollbackOpts(ctx, b, toVersion) {
405
+ const opts = {
406
+ key: keyOf(ctx),
407
+ toVersion,
408
+ actor: actorOf(ctx)
409
+ };
410
+ const env = b["environment"] ?? envOf(ctx);
411
+ if (env !== void 0) opts.environment = env;
412
+ return opts;
413
+ }
414
+ function guarded(adminToken, handler) {
415
+ return async (ctx, next) => {
416
+ const denied = checkAdmin(ctx, adminToken);
417
+ return denied ?? handler(ctx, next);
418
+ };
419
+ }
420
+ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
421
+ const g = (h) => guarded(adminToken, h);
422
+ return [
423
+ // ── config ──────────────────────────────────────────────────
424
+ ["GET", "/admin/config", g(async (ctx) => {
425
+ const rows = await listConfigEntries(envOf(ctx) ?? null, store);
426
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_LISTED", "Config entries", rows);
427
+ })],
428
+ ["GET", "/admin/config/:key", g(async (ctx) => {
429
+ const row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? "all", store);
430
+ return row ? (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_ENTRY", "Config entry", row) : (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "No such config entry");
431
+ })],
432
+ ["PUT", "/admin/config/:key", g(async (ctx) => {
433
+ const b = body(ctx);
434
+ if (!("value" in b)) {
435
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.value is required");
436
+ }
437
+ try {
438
+ const row = await setConfigEntry(
439
+ { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
440
+ store
441
+ );
442
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_SET", "Config entry saved", row);
443
+ } catch (err) {
444
+ return conflictOr(err);
445
+ }
446
+ })],
447
+ ["DELETE", "/admin/config/:key", g(async (ctx) => {
448
+ const ok = await deleteConfigEntry(keyOf(ctx), envOf(ctx) ?? "all", store);
449
+ return (0, import_core.setApiResponse)(ok ? import_core.HTTP.OK : import_core.HTTP.NOT_FOUND, ok ? "DELETED" : "NOT_FOUND", ok ? "Deleted" : "No such config entry");
450
+ })],
451
+ ["GET", "/admin/config/:key/revisions", g(async (ctx) => {
452
+ const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
453
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "REVISIONS", "Config revisions", revs);
454
+ })],
455
+ ["POST", "/admin/config/:key/rollback", g(async (ctx) => {
456
+ const b = body(ctx);
457
+ const toVersion = Number(b["toVersion"]);
458
+ if (!Number.isInteger(toVersion)) {
459
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.toVersion (int) is required");
460
+ }
461
+ const row = await rollbackConfigEntry(
462
+ rollbackOpts(ctx, b, toVersion),
463
+ store
464
+ );
465
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
466
+ })],
467
+ // ── secrets (masked) ────────────────────────────────────────
468
+ ["GET", "/admin/secrets", g(async (ctx) => {
469
+ const rows = await listSecrets(envOf(ctx) ?? null, store);
470
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "SECRETS_LISTED", "Secrets (masked)", rows);
471
+ })],
472
+ ["GET", "/admin/secrets/:key", g(async (ctx) => {
473
+ const row = await getSecret(keyOf(ctx), envOf(ctx) ?? "all", store);
474
+ return row ? (0, import_core.setApiResponse)(import_core.HTTP.OK, "SECRET", "Secret (masked)", row) : (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "No such secret");
475
+ })],
476
+ ["PUT", "/admin/secrets/:key", g(async (ctx) => {
477
+ const b = body(ctx);
478
+ if (typeof b["value"] !== "string") {
479
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.value (string) is required");
480
+ }
481
+ try {
482
+ const row = await setSecret(
483
+ { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
484
+ store,
485
+ encryptor
486
+ );
487
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "SECRET_SET", "Secret saved (masked)", row);
488
+ } catch (err) {
489
+ return conflictOr(err);
490
+ }
491
+ })],
492
+ ["DELETE", "/admin/secrets/:key", g(async (ctx) => {
493
+ const ok = await deleteSecret(keyOf(ctx), envOf(ctx) ?? "all", store);
494
+ return (0, import_core.setApiResponse)(ok ? import_core.HTTP.OK : import_core.HTTP.NOT_FOUND, ok ? "DELETED" : "NOT_FOUND", ok ? "Deleted" : "No such secret");
495
+ })],
496
+ ["GET", "/admin/secrets/:key/revisions", g(async (ctx) => {
497
+ const revs = await listSecretRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
498
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "REVISIONS", "Secret revisions", revs);
499
+ })],
500
+ ["POST", "/admin/secrets/:key/rollback", g(async (ctx) => {
501
+ const b = body(ctx);
502
+ const toVersion = Number(b["toVersion"]);
503
+ if (!Number.isInteger(toVersion)) {
504
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.toVersion (int) is required");
505
+ }
506
+ const row = await rollbackSecret(
507
+ rollbackOpts(ctx, b, toVersion),
508
+ store
509
+ );
510
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
511
+ })],
512
+ // The one plaintext path — behind the same token; POST so the value never
513
+ // lands in a URL/log. Returns the decrypted value.
514
+ ["POST", "/admin/secrets/:key/reveal", g(async (ctx) => {
515
+ const value = await revealSecret(keyOf(ctx), envOf(ctx) ?? "all", store, encryptor);
516
+ return value === null ? (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "No such secret") : (0, import_core.setApiResponse)(import_core.HTTP.OK, "SECRET_REVEALED", "Decrypted secret value", { value });
517
+ })]
518
+ ];
519
+ }
520
+ function conflictOr(err) {
521
+ if (err instanceof import_store.VersionConflictError) {
522
+ return (0, import_core.setApiResponse)(import_core.HTTP.CONFLICT, "VERSION_CONFLICT", err.message, {
523
+ currentVersion: err.currentVersion
524
+ });
525
+ }
526
+ throw err;
527
+ }
528
+
529
+ // src/module.ts
530
+ var ConfigModule = class {
531
+ constructor(store, options = {}) {
532
+ this.store = store;
533
+ this.options = options;
534
+ this.manager = new RemoteConfigManager(store, options);
535
+ }
536
+ store;
537
+ options;
538
+ name = "@fonderie/config";
539
+ manager;
540
+ async install(app) {
541
+ await this.manager.boot();
542
+ app.use(configContextMiddleware(this.manager));
543
+ if (this.options.adminToken) {
544
+ const routes = buildAdminRoutes(
545
+ this.store,
546
+ this.options.adminToken,
547
+ this.options.secretEncryptor ?? noopEncryptor
548
+ );
549
+ for (const [method, path, handler] of routes) {
550
+ app.addRoute(method, path, handler);
551
+ }
552
+ }
553
+ }
554
+ };
197
555
  // Annotate the CommonJS export names for ESM import in node:
198
556
  0 && (module.exports = {
199
557
  CONFIG_MANAGER_KEY,
558
+ ConfigConflictError,
559
+ ConfigModule,
200
560
  RemoteConfigManager,
201
- RemoteConfigModule,
561
+ buildAdminRoutes,
202
562
  configContextMiddleware,
563
+ createAesGcmEncryptor,
203
564
  deleteConfigEntry,
565
+ deleteSecret,
204
566
  getConfig,
205
567
  getConfigEntry,
568
+ getSecret,
206
569
  listConfigEntries,
207
- setConfigEntry
570
+ listConfigRevisions,
571
+ listSecretRevisions,
572
+ listSecrets,
573
+ noopEncryptor,
574
+ revealSecret,
575
+ rollbackConfigEntry,
576
+ rollbackSecret,
577
+ setConfigEntry,
578
+ setSecret
208
579
  });
209
580
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/manager.ts","../src/middlewares/config-context.ts","../src/module.ts","../src/services/config.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { RemoteConfigModule } from './module';\nexport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nexport { configContextMiddleware, getConfig } from './middlewares/config-context';\nexport {\n\tlistConfigEntries,\n\tgetConfigEntry,\n\tsetConfigEntry,\n\tdeleteConfigEntry,\n} from './services/config';\n\nexport type { IConfigEntry, IConfigSnapshot } from './types';\nexport type { IRemoteConfigOptions } from './config';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IRemoteConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\n\nimport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nimport { configContextMiddleware } from './middlewares/config-context';\n\nexport class RemoteConfigModule implements IFonderieModule {\n\treadonly name = '@fonderie/config';\n\treadonly manager: RemoteConfigManager;\n\n\tconstructor(store: IStoreAdapter, options: IRemoteConfigOptions = {}) {\n\t\tthis.manager = new RemoteConfigManager(store, options);\n\t}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait this.manager.boot();\n\t\tapp.use(configContextMiddleware(this.manager));\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigEntry } from '../types';\n\nconst SELECT_ENTRY = `\n\tSELECT\n\t\tkey,\n\t\tvalue,\n\t\tenvironment,\n\t\tdescription,\n\t\tactive,\n\t\tupdated_at AS \"updatedAt\"\n\tFROM fonderie_config`;\n\nexport async function listConfigEntries(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry[]> {\n\treturn environment\n\t\t? store.query<IConfigEntry>(\n\t\t\t\t`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<IConfigEntry>(`${SELECT_ENTRY} ORDER BY environment, key`);\n}\n\nexport async function getConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry | null> {\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\nexport async function setConfigEntry(\n\topts: {\n\t\tkey: string;\n\t\tvalue: unknown;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\tconst env = opts.environment ?? 'all';\n\tconst raw = typeof opts.value === 'string' ? opts.value : JSON.stringify(opts.value);\n\tconst active = opts.active ?? true;\n\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`INSERT INTO fonderie_config (key, value, environment, description, active)\n\t\tVALUES ($1, $2, $3, $4, $5)\n\t\tON CONFLICT (key, environment) DO UPDATE SET\n\t\t\tvalue = $2,\n\t\t\tdescription = COALESCE($4, fonderie_config.description),\n\t\t\tactive = $5,\n\t\t\tupdated_at = now()\n\t\tRETURNING\n\t\t\tkey, value, environment, description, active,\n\t\t\tupdated_at AS \"updatedAt\"`,\n\t\t[opts.key, raw, env, opts.description ?? null, active],\n\t);\n\tif (!row) throw new Error('Failed to upsert config entry');\n\treturn row;\n}\n\nexport async function deleteConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/manager.ts","../src/middlewares/config-context.ts","../src/admin.ts","../src/crypto.ts","../src/services/versioned.ts","../src/services/config.ts","../src/services/secrets.ts","../src/module.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { ConfigModule } from './module';\nexport { buildAdminRoutes } from './admin';export { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nexport { configContextMiddleware, getConfig } from './middlewares/config-context';\nexport {\n\tlistConfigEntries,\n\tgetConfigEntry,\n\tsetConfigEntry,\n\tdeleteConfigEntry,\n\trollbackConfigEntry,\n\tlistConfigRevisions,\n\tConfigConflictError,\n} from './services/config';\n\nexport type { IConfigEntry, IConfigRevision, IConfigSnapshot } from './types';\n\n// Secrets — the masked/encryptable kind (same lifecycle as config, separate\n// table + read path). `revealSecret` is the only path that returns plaintext.\nexport {\n\tlistSecrets,\n\tgetSecret,\n\trevealSecret,\n\tsetSecret,\n\trollbackSecret,\n\tlistSecretRevisions,\n\tdeleteSecret,\n} from './services/secrets';\nexport type { ISecretEntry, ISecretRevision } from './types';\nexport { noopEncryptor, createAesGcmEncryptor } from './crypto';\nexport type { ISecretEncryptor } from './crypto';\nexport type { IConfigOptions } from './config';\n","import pg from 'pg';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\nconst NOTIFY_CHANNEL = 'fonderie_config_changed';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate listenClient: pg.Client | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\tprivate connectionUrl: string | undefined;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t\tthis.connectionUrl = options.connectionUrl;\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\t// Poll floor — catches a missed NOTIFY, cold start, or no LISTEN client.\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t\t// Push fast-path — refresh within ms of a write (broadcast invalidation).\n\t\tif (this.connectionUrl) await this.startListening();\n\t}\n\n\tprivate async startListening(): Promise<void> {\n\t\ttry {\n\t\t\tthis.listenClient = new pg.Client(this.connectionUrl);\n\t\t\tawait this.listenClient.connect();\n\t\t\tawait this.listenClient.query(`LISTEN ${NOTIFY_CHANNEL}`);\n\t\t\tthis.listenClient.on('notification', () => {\n\t\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t\t});\n\t\t\tthis.listenClient.on('error', (err) =>\n\t\t\t\tconsole.error('[config] listen client error:', err.message),\n\t\t\t);\n\t\t} catch (err) {\n\t\t\t// Non-fatal: fall back to the poll floor if LISTEN can't be established.\n\t\t\tconsole.error('[config] failed to start LISTEN, falling back to poll:', err);\n\t\t\tthis.listenClient = null;\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t\tvoid this.listenClient?.end();\n\t\tthis.listenClient = null;\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISecretEncryptor } from './crypto';\nimport { noopEncryptor } from './crypto';\nimport {\n\tlistConfigEntries,\n\tgetConfigEntry,\n\tsetConfigEntry,\n\tdeleteConfigEntry,\n\trollbackConfigEntry,\n\tlistConfigRevisions,\n\tConfigConflictError,\n} from './services/config';\nimport {\n\tlistSecrets,\n\tgetSecret,\n\trevealSecret,\n\tsetSecret,\n\tdeleteSecret,\n\trollbackSecret,\n\tlistSecretRevisions,\n} from './services/secrets';\n\n// Bootstrap-token guard. Returns null when authorized, else the 401/503 Response.\n// The admin surface is only registered when a token is configured, so a missing\n// config here is defensive.\nfunction checkAdmin(ctx: IFonderieContext, adminToken: string): Response | null {\n\tconst header = ctx.request.headers.get('authorization') ?? '';\n\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\tif (!token || token !== adminToken) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\t}\n\treturn null;\n}\n\n// The actor recorded on writes/audit — an optional `X-Actor` header lets the\n// caller (or the LLM) identify who; defaults to a generic admin label.\nfunction actorOf(ctx: IFonderieContext): string {\n\treturn ctx.request.headers.get('x-actor') || 'admin-token';\n}\n\nfunction envOf(ctx: IFonderieContext): string | undefined {\n\treturn new URL(ctx.request.url).searchParams.get('environment') ?? undefined;\n}\n\nfunction keyOf(ctx: IFonderieContext): string {\n\treturn ctx.meta.params?.['key'] ?? '';\n}\n\nfunction body(ctx: IFonderieContext): Record<string, unknown> {\n\treturn (ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n}\n\n// Build the shared optional write fields, omitting any that aren't present\n// (exactOptionalPropertyTypes: never assign `undefined`).\nfunction writeOpts(\n\tctx: IFonderieContext,\n\tb: Record<string, unknown>,\n): { environment?: string; description?: string; ifVersion?: number; actor: string } {\n\tconst opts: { environment?: string; description?: string; ifVersion?: number; actor: string } = {\n\t\tactor: actorOf(ctx),\n\t};\n\tconst env = (b['environment'] as string | undefined) ?? envOf(ctx);\n\tif (env !== undefined) opts.environment = env;\n\tif (typeof b['description'] === 'string') opts.description = b['description'];\n\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'] as number;\n\treturn opts;\n}\n\n// Rollback opts, omitting `environment` when not supplied.\nfunction rollbackOpts(\n\tctx: IFonderieContext,\n\tb: Record<string, unknown>,\n\ttoVersion: number,\n): { key: string; environment?: string; toVersion: number; actor: string } {\n\tconst opts: { key: string; environment?: string; toVersion: number; actor: string } = {\n\t\tkey: keyOf(ctx),\n\t\ttoVersion,\n\t\tactor: actorOf(ctx),\n\t};\n\tconst env = (b['environment'] as string | undefined) ?? envOf(ctx);\n\tif (env !== undefined) opts.environment = env;\n\treturn opts;\n}\n\n// Wrap a handler in the admin-token guard.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst denied = checkAdmin(ctx, adminToken);\n\t\treturn denied ?? handler(ctx, next);\n\t};\n}\n\n// Build the admin route table. Registered by ConfigModule.install only when an\n// adminToken is configured.\nexport function buildAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n\tencryptor: ISecretEncryptor = noopEncryptor,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t// ── config ──────────────────────────────────────────────────\n\t\t['GET', '/admin/config', g(async (ctx) => {\n\t\t\tconst rows = await listConfigEntries(envOf(ctx) ?? null, store);\n\t\t\treturn setApiResponse(HTTP.OK, 'CONFIG_LISTED', 'Config entries', rows);\n\t\t})],\n\t\t['GET', '/admin/config/:key', g(async (ctx) => {\n\t\t\tconst row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'CONFIG_ENTRY', 'Config entry', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such config entry');\n\t\t})],\n\t\t['PUT', '/admin/config/:key', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (!('value' in b)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst row = await setConfigEntry(\n\t\t\t\t\t{ key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) },\n\t\t\t\t\tstore,\n\t\t\t\t);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'CONFIG_SET', 'Config entry saved', row);\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/config/:key', g(async (ctx) => {\n\t\t\tconst ok = await deleteConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such config entry');\n\t\t})],\n\t\t['GET', '/admin/config/:key/revisions', g(async (ctx) => {\n\t\t\tconst revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Config revisions', revs);\n\t\t})],\n\t\t['POST', '/admin/config/:key/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackConfigEntry(\n\t\t\t\trollbackOpts(ctx, b, toVersion),\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\n\t\t// ── secrets (masked) ────────────────────────────────────────\n\t\t['GET', '/admin/secrets', g(async (ctx) => {\n\t\t\tconst rows = await listSecrets(envOf(ctx) ?? null, store);\n\t\t\treturn setApiResponse(HTTP.OK, 'SECRETS_LISTED', 'Secrets (masked)', rows);\n\t\t})],\n\t\t['GET', '/admin/secrets/:key', g(async (ctx) => {\n\t\t\tconst row = await getSecret(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'SECRET', 'Secret (masked)', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such secret');\n\t\t})],\n\t\t['PUT', '/admin/secrets/:key', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['value'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst row = await setSecret(\n\t\t\t\t\t{ key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) },\n\t\t\t\t\tstore,\n\t\t\t\t\tencryptor,\n\t\t\t\t);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'SECRET_SET', 'Secret saved (masked)', row);\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/secrets/:key', g(async (ctx) => {\n\t\t\tconst ok = await deleteSecret(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such secret');\n\t\t})],\n\t\t['GET', '/admin/secrets/:key/revisions', g(async (ctx) => {\n\t\t\tconst revs = await listSecretRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store);\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Secret revisions', revs);\n\t\t})],\n\t\t['POST', '/admin/secrets/:key/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackSecret(\n\t\t\t\trollbackOpts(ctx, b, toVersion),\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t\t// The one plaintext path — behind the same token; POST so the value never\n\t\t// lands in a URL/log. Returns the decrypted value.\n\t\t['POST', '/admin/secrets/:key/reveal', g(async (ctx) => {\n\t\t\tconst value = await revealSecret(keyOf(ctx), envOf(ctx) ?? 'all', store, encryptor);\n\t\t\treturn value === null\n\t\t\t\t? setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such secret')\n\t\t\t\t: setApiResponse(HTTP.OK, 'SECRET_REVEALED', 'Decrypted secret value', { value });\n\t\t})],\n\t];\n}\n\n// Map a lost optimistic-concurrency write to a 409 (reject-and-retry), else rethrow.\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof ConfigConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\n\n// Pluggable at-rest encryption for secrets. The store only ever sees the output\n// of `encrypt`; the reveal path calls `decrypt`. Bring your own (KMS-backed) or\n// use the built-ins below.\nexport interface ISecretEncryptor {\n\tencrypt(plain: string): string;\n\tdecrypt(cipher: string): string;\n}\n\n// Default: no encryption. Secrets are still masked in admin reads and isolated\n// in their own table/authz — but plaintext at rest. Configure a real encryptor\n// (`createAesGcmEncryptor`) for a production deployment.\nexport const noopEncryptor: ISecretEncryptor = {\n\tencrypt: (plain) => plain,\n\tdecrypt: (cipher) => cipher,\n};\n\n// AES-256-GCM. `key` is 32 bytes as a 64-char hex string (e.g.\n// `openssl rand -hex 32`). Output is `iv:tag:ciphertext`, all hex. Authenticated\n// (tampering is detected on decrypt).\nexport function createAesGcmEncryptor(keyHex: string): ISecretEncryptor {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tif (key.length !== 32) {\n\t\tthrow new Error('[config] secret encryptor key must be 32 bytes (64 hex chars)');\n\t}\n\treturn {\n\t\tencrypt(plain) {\n\t\t\tconst iv = randomBytes(12);\n\t\t\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\t\t\tconst enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);\n\t\t\tconst tag = cipher.getAuthTag();\n\t\t\treturn `${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;\n\t\t},\n\t\tdecrypt(cipher) {\n\t\t\tconst [ivHex, tagHex, dataHex] = cipher.split(':');\n\t\t\tif (!ivHex || !tagHex || !dataHex) {\n\t\t\t\tthrow new Error('[config] malformed secret ciphertext');\n\t\t\t}\n\t\t\tconst decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));\n\t\t\tdecipher.setAuthTag(Buffer.from(tagHex, 'hex'));\n\t\t\treturn Buffer.concat([\n\t\t\t\tdecipher.update(Buffer.from(dataHex, 'hex')),\n\t\t\t\tdecipher.final(),\n\t\t\t]).toString('utf8');\n\t\t},\n\t};\n}\n","// The versioned-resource primitive now lives in @fonderie/store (pure Postgres\n// machinery, reused by config, secrets, and courier templates). Re-exported here\n// so config's internal imports are unchanged and `ConfigConflictError` stays a\n// stable public name (an alias of the shared `VersionConflictError`).\nexport {\n\tversionedWrite,\n\tversionedRollback,\n\tVersionConflictError,\n\tVersionConflictError as ConfigConflictError,\n} from '@fonderie/store';\nexport type { IVersionedResource } from '@fonderie/store';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigEntry, IConfigRevision } from '../types';\nimport type { IVersionedResource } from './versioned';\nimport { versionedWrite, versionedRollback } from './versioned';\n\n// Re-exported for the public API (the shared primitive owns the class).\nexport { ConfigConflictError } from './versioned';\n\nconst ENTRY_COLS = `\n\tkey,\n\tvalue,\n\tenvironment,\n\tdescription,\n\tactive,\n\tversion,\n\tupdated_by AS \"updatedBy\",\n\tupdated_at AS \"updatedAt\"`;\n\nconst SELECT_ENTRY = `SELECT ${ENTRY_COLS} FROM fonderie_config`;\n\nconst CONFIG_TABLE: IVersionedResource = {\n\ttable: 'fonderie_config',\n\trevisions: 'fonderie_config_revisions',\n\tchannel: 'fonderie_config_changed',\n\tkeyColumns: ['key', 'environment'],\n\tcontentColumns: ['value'],\n\tmetaColumns: ['description', 'active'],\n\treturning: ENTRY_COLS,\n};\n\nexport async function listConfigEntries(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry[]> {\n\treturn environment\n\t\t? store.query<IConfigEntry>(\n\t\t\t\t`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<IConfigEntry>(`${SELECT_ENTRY} ORDER BY environment, key`);\n}\n\nexport async function getConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry | null> {\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\n// Upsert a config entry. When `ifVersion` is given, the write is guarded by\n// optimistic concurrency (else `ConfigConflictError`). Values are JSON-encoded;\n// version bump, revision, advisory lock and push-notify are the shared primitive.\nexport async function setConfigEntry(\n\topts: {\n\t\tkey: string;\n\t\tvalue: unknown;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\tconst rawValue = typeof opts.value === 'string' ? opts.value : JSON.stringify(opts.value);\n\tconst data: Record<string, unknown> = { value: rawValue, active: opts.active ?? true };\n\tif (opts.description !== undefined) data['description'] = opts.description;\n\treturn versionedWrite<IConfigEntry>(CONFIG_TABLE, store, {\n\t\tkey: opts.key,\n\t\tscope: opts.environment ?? 'all',\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackConfigEntry(\n\topts: { key: string; environment?: string; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\treturn versionedRollback<IConfigEntry>(CONFIG_TABLE, store, {\n\t\tkey: opts.key,\n\t\tscope: opts.environment ?? 'all',\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listConfigRevisions(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigRevision[]> {\n\treturn store.query<IConfigRevision>(\n\t\t`SELECT key, environment, value, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_config_revisions\n\t\t WHERE key = $1 AND environment = $2\n\t\t ORDER BY version DESC`,\n\t\t[key, environment],\n\t);\n}\n\nexport async function deleteConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISecretEntry, ISecretRevision } from '../types';\nimport type { ISecretEncryptor } from '../crypto';\nimport { noopEncryptor } from '../crypto';\nimport type { IVersionedResource } from './versioned';\nimport { versionedWrite, versionedRollback } from './versioned';\n\n// Metadata only — the value column is deliberately never selected here, so a\n// secret's plaintext can't leak through admin list/get. Only `revealSecret`\n// reads + decrypts the value.\nconst META_COLS = `\n\tkey,\n\tenvironment,\n\tdescription,\n\tactive,\n\tversion,\n\tupdated_by AS \"updatedBy\",\n\tupdated_at AS \"updatedAt\"`;\n\nconst SECRET_TABLE: IVersionedResource = {\n\ttable: 'fonderie_secrets',\n\trevisions: 'fonderie_secret_revisions',\n\tchannel: 'fonderie_secrets_changed',\n\tkeyColumns: ['key', 'environment'],\n\tcontentColumns: ['value'],\n\tmetaColumns: ['description', 'active'],\n\treturning: META_COLS, // masked — no value\n};\n\nexport async function listSecrets(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<ISecretEntry[]> {\n\treturn environment\n\t\t? store.query<ISecretEntry>(\n\t\t\t\t`SELECT ${META_COLS} FROM fonderie_secrets\n\t\t\t\t WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<ISecretEntry>(\n\t\t\t\t`SELECT ${META_COLS} FROM fonderie_secrets ORDER BY environment, key`,\n\t\t\t);\n}\n\n// Masked read — metadata, never the value.\nexport async function getSecret(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<ISecretEntry | null> {\n\tconst [row] = await store.query<ISecretEntry>(\n\t\t`SELECT ${META_COLS} FROM fonderie_secrets WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\n// The ONE path that returns a plaintext value — decrypts on the way out. Keep it\n// behind the tightest authz; never log its result.\nexport async function revealSecret(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n\tencryptor: ISecretEncryptor = noopEncryptor,\n): Promise<string | null> {\n\tconst [row] = await store.query<{ value: string }>(\n\t\t`SELECT value FROM fonderie_secrets WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ? encryptor.decrypt(row.value) : null;\n}\n\n// Write a secret — same versioned primitive as config, but the value is\n// encrypted at rest and returned masked (metadata only).\nexport async function setSecret(\n\topts: {\n\t\tkey: string;\n\t\tvalue: string;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n\tencryptor: ISecretEncryptor = noopEncryptor,\n): Promise<ISecretEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tvalue: encryptor.encrypt(opts.value),\n\t\tactive: opts.active ?? true,\n\t};\n\tif (opts.description !== undefined) data['description'] = opts.description;\n\treturn versionedWrite<ISecretEntry>(SECRET_TABLE, store, {\n\t\tkey: opts.key,\n\t\tscope: opts.environment ?? 'all',\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackSecret(\n\topts: { key: string; environment?: string; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ISecretEntry> {\n\treturn versionedRollback<ISecretEntry>(SECRET_TABLE, store, {\n\t\tkey: opts.key,\n\t\tscope: opts.environment ?? 'all',\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listSecretRevisions(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<ISecretRevision[]> {\n\treturn store.query<ISecretRevision>(\n\t\t`SELECT key, environment, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_secret_revisions\n\t\t WHERE key = $1 AND environment = $2\n\t\t ORDER BY version DESC`,\n\t\t[key, environment],\n\t);\n}\n\nexport async function deleteSecret(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_secrets WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\n\nimport { RemoteConfigManager } from './manager';\nimport { configContextMiddleware } from './middlewares/config-context';\nimport { buildAdminRoutes } from './admin';\nimport { noopEncryptor } from './crypto';\n\nexport class ConfigModule implements IFonderieModule {\n\treadonly name = '@fonderie/config';\n\treadonly manager: RemoteConfigManager;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate options: IConfigOptions = {},\n\t) {\n\t\tthis.manager = new RemoteConfigManager(store, options);\n\t}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait this.manager.boot();\n\t\tapp.use(configContextMiddleware(this.manager));\n\n\t\t// Admin HTTP surface — only when a bootstrap token is configured\n\t\t// (no token, no exposed admin routes: fail-closed).\n\t\tif (this.options.adminToken) {\n\t\t\tconst routes = buildAdminRoutes(\n\t\t\t\tthis.store,\n\t\t\t\tthis.options.adminToken,\n\t\t\t\tthis.options.secretEncryptor ?? noopEncryptor,\n\t\t\t);\n\t\t\tfor (const [method, path, handler] of routes) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["pg"]}