@fonderie/config 4.0.0 → 5.1.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/README.md CHANGED
@@ -102,14 +102,14 @@ Zero runtime dependencies. Peers only: `@fonderie/core`, `@fonderie/store`, and
102
102
  You've shipped this plumbing before — auth, teams, billing, messaging — and the
103
103
  next project will ask for it again. Fonderie packages it once: plain TypeScript
104
104
  modules for
105
- [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
105
+ [`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
106
106
  PostgreSQL-backed, self-hosted, MIT. No external control plane, no per-seat
107
107
  anything. Register the modules you need; skip the ones you don't.
108
108
 
109
109
  **This package owns** how behavior — and secrets — change without a deploy.
110
110
 
111
111
  Browse the whole set at
112
- [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
112
+ [fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
113
113
  [@fonderiejs](https://x.com/fonderiejs)
114
114
 
115
115
  ## License
package/brain/outcomes.md CHANGED
@@ -69,12 +69,12 @@ Raw SQL ships in `node_modules/@fonderie/config/dist/migrations/sql/` — read i
69
69
 
70
70
  | Method | Path | Middleware chain (auth / validation / handler) |
71
71
  |---|---|---|
72
- | GET | `/admin/config` | `g(async (ctx) => { const rows = await listConfigEntries(envOf(ctx) ?? null, store); return setApiResponse(HTTP.OK, 'CONFIG_LISTED', 'Config entries', rows); })` |
72
+ | GET | `/admin/config` | `g(async (ctx) => { const rows = await listConfigEntries(envOf(ctx) ?? null, store); return setApiResponse(HTTP.OK, 'CONFIG_LISTED', 'Config entries', rows.map(withParsedValue)); })` |
73
73
  | DELETE | `/admin/config/:key` | `g(async (ctx) => { const ok = await deleteConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such config entry'); })` |
74
- | GET | `/admin/config/:key` | `g(async (ctx) => { const row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store); return row ? setApiResponse(HTTP.OK, 'CONFIG_ENTRY', 'Config entry', row) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such config entry'); })` |
75
- | PUT | `/admin/config/:key` | `g(async (ctx) => { const b = body(ctx); if (!('value' in b)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value is required'); } try { const row = await setConfigEntry( { key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) }, store, ); return setApiResponse(HTTP.OK, 'CONFIG_SET', 'Config entry saved', row); } catch (err) { return conflictOr(err); } })` |
76
- | GET | `/admin/config/:key/revisions` | `g(async (ctx) => { const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(HTTP.OK, 'REVISIONS', 'Config revisions', revs); })` |
77
- | POST | `/admin/config/:key/rollback` | `g(async (ctx) => { const b = body(ctx); const toVersion = Number(b['toVersion']); if (!Number.isInteger(toVersion)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required'); } const row = await rollbackConfigEntry( rollbackOpts(ctx, b, toVersion), store, ); return setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row); })` |
74
+ | GET | `/admin/config/:key` | `g(async (ctx) => { const row = await getConfigEntry(keyOf(ctx), envOf(ctx) ?? 'all', store); return row ? setApiResponse(HTTP.OK, 'CONFIG_ENTRY', 'Config entry', withParsedValue(row)) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such config entry'); })` |
75
+ | PUT | `/admin/config/:key` | `g(async (ctx) => { const b = body(ctx); if (!('value' in b)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.value is required'); } try { const row = await setConfigEntry( { key: keyOf(ctx), value: b['value'], ...writeOpts(ctx, b) }, store, ); return setApiResponse(HTTP.OK, 'CONFIG_SET', 'Config entry saved', withParsedValue(row)); } catch (err) { return conflictOr(err); } })` |
76
+ | GET | `/admin/config/:key/revisions` | `g(async (ctx) => { const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(HTTP.OK, 'REVISIONS', 'Config revisions', revs.map(withParsedValue)); })` |
77
+ | POST | `/admin/config/:key/rollback` | `g(async (ctx) => { const b = body(ctx); const toVersion = Number(b['toVersion']); if (!Number.isInteger(toVersion)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required'); } const row = await rollbackConfigEntry( rollbackOpts(ctx, b, toVersion), store, ); return setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, withParsedValue(row)); })` |
78
78
  | GET | `/admin/secrets` | `g(async (ctx) => { const rows = await listSecrets(envOf(ctx) ?? null, store); return setApiResponse(HTTP.OK, 'SECRETS_LISTED', 'Secrets (masked)', rows); })` |
79
79
  | DELETE | `/admin/secrets/:key` | `g(async (ctx) => { const ok = await deleteSecret(keyOf(ctx), envOf(ctx) ?? 'all', store); return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such secret'); })` |
80
80
  | GET | `/admin/secrets/:key` | `g(async (ctx) => { const row = await getSecret(keyOf(ctx), envOf(ctx) ?? 'all', store); return row ? setApiResponse(HTTP.OK, 'SECRET', 'Secret (masked)', row) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such secret'); })` |
@@ -11,6 +11,7 @@ new ConfigModule(store: IStoreAdapter, options?: IConfigOptions): ConfigModule
11
11
  .name: "@fonderie/config"
12
12
  .manager: RemoteConfigManager
13
13
  .install(app: IFonderieApp): Promise<void>
14
+ .checkReadiness(): IReadinessProblem[]
14
15
 
15
16
  function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): [string, string, Middleware][]
16
17
 
package/dist/index.cjs CHANGED
@@ -180,6 +180,7 @@ function getConfig(ctx, key, fallback = null) {
180
180
  }
181
181
 
182
182
  // src/admin.ts
183
+ var import_node_crypto2 = require("crypto");
183
184
  var import_core = require("@fonderie/core");
184
185
 
185
186
  // src/crypto.ts
@@ -220,6 +221,14 @@ function createAesGcmEncryptor(keyHex) {
220
221
  var import_store = require("@fonderie/store");
221
222
 
222
223
  // src/services/config.ts
224
+ function withParsedValue(row) {
225
+ if (typeof row.value !== "string") return row;
226
+ try {
227
+ return { ...row, value: JSON.parse(row.value) };
228
+ } catch {
229
+ return row;
230
+ }
231
+ }
223
232
  var ENTRY_COLS = `
224
233
  key,
225
234
  value,
@@ -374,11 +383,17 @@ async function deleteSecret(key, environment, store) {
374
383
  function checkAdmin(ctx, adminToken) {
375
384
  const header = ctx.request.headers.get("authorization") ?? "";
376
385
  const token = header.startsWith("Bearer ") ? header.slice(7) : "";
377
- if (!token || token !== adminToken) {
386
+ if (!token || !safeTokenEqual(token, adminToken)) {
378
387
  return (0, import_core.setApiResponse)(import_core.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
379
388
  }
380
389
  return null;
381
390
  }
391
+ function safeTokenEqual(a, b) {
392
+ const bufA = Buffer.from(a);
393
+ const bufB = Buffer.from(b);
394
+ if (bufA.length !== bufB.length) return false;
395
+ return (0, import_node_crypto2.timingSafeEqual)(bufA, bufB);
396
+ }
382
397
  function actorOf(ctx) {
383
398
  return ctx.request.headers.get("x-actor") || "admin-token";
384
399
  }
@@ -398,6 +413,7 @@ function writeOpts(ctx, b) {
398
413
  const env = b["environment"] ?? envOf(ctx);
399
414
  if (env !== void 0) opts.environment = env;
400
415
  if (typeof b["description"] === "string") opts.description = b["description"];
416
+ if (typeof b["active"] === "boolean") opts.active = b["active"];
401
417
  if (typeof b["ifVersion"] === "number") opts.ifVersion = b["ifVersion"];
402
418
  return opts;
403
419
  }
@@ -423,11 +439,11 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
423
439
  // ── config ──────────────────────────────────────────────────
424
440
  ["GET", "/admin/config", g(async (ctx) => {
425
441
  const rows = await listConfigEntries(envOf(ctx) ?? null, store);
426
- return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_LISTED", "Config entries", rows);
442
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_LISTED", "Config entries", rows.map(withParsedValue));
427
443
  })],
428
444
  ["GET", "/admin/config/:key", g(async (ctx) => {
429
445
  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");
446
+ return row ? (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_ENTRY", "Config entry", withParsedValue(row)) : (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "No such config entry");
431
447
  })],
432
448
  ["PUT", "/admin/config/:key", g(async (ctx) => {
433
449
  const b = body(ctx);
@@ -439,7 +455,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
439
455
  { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
440
456
  store
441
457
  );
442
- return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_SET", "Config entry saved", row);
458
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "CONFIG_SET", "Config entry saved", withParsedValue(row));
443
459
  } catch (err) {
444
460
  return conflictOr(err);
445
461
  }
@@ -450,7 +466,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
450
466
  })],
451
467
  ["GET", "/admin/config/:key/revisions", g(async (ctx) => {
452
468
  const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
453
- return (0, import_core.setApiResponse)(import_core.HTTP.OK, "REVISIONS", "Config revisions", revs);
469
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "REVISIONS", "Config revisions", revs.map(withParsedValue));
454
470
  })],
455
471
  ["POST", "/admin/config/:key/rollback", g(async (ctx) => {
456
472
  const b = body(ctx);
@@ -462,7 +478,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
462
478
  rollbackOpts(ctx, b, toVersion),
463
479
  store
464
480
  );
465
- return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
481
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, withParsedValue(row));
466
482
  })],
467
483
  // ── secrets (masked) ────────────────────────────────────────
468
484
  ["GET", "/admin/secrets", g(async (ctx) => {
@@ -527,6 +543,8 @@ function conflictOr(err) {
527
543
  }
528
544
 
529
545
  // src/module.ts
546
+ var MIN_ADMIN_TOKEN_LENGTH = 32;
547
+ var PLACEHOLDER_TOKEN = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;
530
548
  var ConfigModule = class {
531
549
  constructor(store, options = {}) {
532
550
  this.store = store;
@@ -551,6 +569,38 @@ var ConfigModule = class {
551
569
  }
552
570
  }
553
571
  }
572
+ // Reported by FonderieApp.checkProductionReadiness. Only the admin surface
573
+ // carries a secret worth guarding; when no adminToken is set the surface
574
+ // isn't registered at all (fail-closed), so there's nothing to flag.
575
+ checkReadiness() {
576
+ const problems = [];
577
+ if (!this.options.secretEncryptor) {
578
+ const inProd = process.env["NODE_ENV"] === "production";
579
+ const secretsExposed = Boolean(this.options.adminToken);
580
+ problems.push({
581
+ module: this.name,
582
+ severity: inProd && secretsExposed ? "error" : "warning",
583
+ message: "no secretEncryptor configured \u2014 secrets are stored plaintext at rest; use createAesGcmEncryptor" + (inProd && secretsExposed ? " (required in production when the secrets admin surface is enabled)" : " for production")
584
+ });
585
+ }
586
+ const token = this.options.adminToken;
587
+ if (token) {
588
+ if (token.length < MIN_ADMIN_TOKEN_LENGTH) {
589
+ problems.push({
590
+ module: this.name,
591
+ severity: "error",
592
+ message: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`
593
+ });
594
+ } else if (PLACEHOLDER_TOKEN.test(token)) {
595
+ problems.push({
596
+ module: this.name,
597
+ severity: "error",
598
+ message: "adminToken looks like a placeholder or dev-default value"
599
+ });
600
+ }
601
+ }
602
+ return problems;
603
+ }
554
604
  };
555
605
  // Annotate the CommonJS export names for ESM import in node:
556
606
  0 && (module.exports = {
@@ -1 +1 @@
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"]}
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 { timingSafeEqual } from 'node:crypto';\n\nimport 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\twithParsedValue,\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 || !safeTokenEqual(token, adminToken)) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\t}\n\treturn null;\n}\n\n// Constant-time comparison so a wrong token can't be recovered byte-by-byte\n// from response-timing. Encode to bytes and length-guard first: timingSafeEqual\n// throws on unequal lengths, and that early return is itself acceptable — the\n// secret's length is not the sensitive part.\nfunction safeTokenEqual(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\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; active?: boolean; ifVersion?: number; actor: string } {\n\tconst opts: {\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor: string;\n\t} = {\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\t// Previously dropped here, silently forcing active=true on every HTTP\n\t// write while list reads filter on it.\n\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\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.map(withParsedValue));\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', withParsedValue(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', withParsedValue(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.map(withParsedValue));\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}`, withParsedValue(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\n// The column stores JSON-encoded text; the runtime read path (manager)\n// parses it with a raw-string fallback. The admin surface must serve the\n// same parsed shape — otherwise setConfig({a: 1}) reads back as the string\n// '{\"a\":1}' and the shipped editor re-stringifies it into a degradation\n// loop on every save.\nexport function withParsedValue<T extends { value: unknown }>(row: T): T {\n\tif (typeof row.value !== 'string') return row;\n\ttry {\n\t\treturn { ...row, value: JSON.parse(row.value) };\n\t} catch {\n\t\treturn row;\n\t}\n}\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, IReadinessProblem } 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\n// A weak admin token guards the plaintext-secret reveal surface, so hold it to\n// the same bar as an auth signing secret: 32+ chars and not a copy-pasted\n// placeholder. Mirrors the jwtSecret checks in @fonderie/auth's config-guard.\nconst MIN_ADMIN_TOKEN_LENGTH = 32;\nconst PLACEHOLDER_TOKEN =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\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\t// Reported by FonderieApp.checkProductionReadiness. Only the admin surface\n\t// carries a secret worth guarding; when no adminToken is set the surface\n\t// isn't registered at all (fail-closed), so there's nothing to flag.\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\t// Secrets are stored plaintext at rest unless an encryptor is configured.\n\t\t// It's a hard error in production when the admin secrets surface is enabled\n\t\t// (a plaintext secret is revealable over the API) — this fails the boot gate.\n\t\t// Otherwise (dev, or no admin surface) it's a warning so back-compatible\n\t\t// deployments that never expose secrets aren't broken.\n\t\tif (!this.options.secretEncryptor) {\n\t\t\tconst inProd = process.env['NODE_ENV'] === 'production';\n\t\t\tconst secretsExposed = Boolean(this.options.adminToken);\n\t\t\tproblems.push({\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: inProd && secretsExposed ? 'error' : 'warning',\n\t\t\t\tmessage:\n\t\t\t\t\t'no secretEncryptor configured — secrets are stored plaintext at rest; ' +\n\t\t\t\t\t'use createAesGcmEncryptor' +\n\t\t\t\t\t(inProd && secretsExposed\n\t\t\t\t\t\t? ' (required in production when the secrets admin surface is enabled)'\n\t\t\t\t\t\t: ' for production'),\n\t\t\t});\n\t\t}\n\n\t\tconst token = this.options.adminToken;\n\t\tif (token) {\n\t\t\tif (token.length < MIN_ADMIN_TOKEN_LENGTH) {\n\t\t\t\tproblems.push({\n\t\t\t\t\tmodule: this.name,\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`,\n\t\t\t\t});\n\t\t\t} else if (PLACEHOLDER_TOKEN.test(token)) {\n\t\t\t\tproblems.push({\n\t\t\t\t\tmodule: this.name,\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: 'adminToken looks like a placeholder or dev-default value',\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn problems;\n\t}\n}\n"],"mappings":"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","names":["pg","import_node_crypto"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { IFonderieModule, IFonderieApp, Middleware } from '@fonderie/core';
1
+ import { IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
2
  import { IStoreAdapter } from '@fonderie/store';
3
3
  export { VersionConflictError as ConfigConflictError } from '@fonderie/store';
4
4
  import { R as RemoteConfigManager, I as IConfigOptions, a as ISecretEncryptor } from './index-CLaJRR69.cjs';
@@ -13,6 +13,7 @@ declare class ConfigModule implements IFonderieModule {
13
13
  readonly manager: RemoteConfigManager;
14
14
  constructor(store: IStoreAdapter, options?: IConfigOptions);
15
15
  install(app: IFonderieApp): Promise<void>;
16
+ checkReadiness(): IReadinessProblem[];
16
17
  }
17
18
 
18
19
  declare function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): Array<[string, string, Middleware]>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IFonderieModule, IFonderieApp, Middleware } from '@fonderie/core';
1
+ import { IFonderieModule, IFonderieApp, IReadinessProblem, Middleware } from '@fonderie/core';
2
2
  import { IStoreAdapter } from '@fonderie/store';
3
3
  export { VersionConflictError as ConfigConflictError } from '@fonderie/store';
4
4
  import { R as RemoteConfigManager, I as IConfigOptions, a as ISecretEncryptor } from './index-CLaJRR69.js';
@@ -13,6 +13,7 @@ declare class ConfigModule implements IFonderieModule {
13
13
  readonly manager: RemoteConfigManager;
14
14
  constructor(store: IStoreAdapter, options?: IConfigOptions);
15
15
  install(app: IFonderieApp): Promise<void>;
16
+ checkReadiness(): IReadinessProblem[];
16
17
  }
17
18
 
18
19
  declare function buildAdminRoutes(store: IStoreAdapter, adminToken: string, encryptor?: ISecretEncryptor): Array<[string, string, Middleware]>;
package/dist/index.js CHANGED
@@ -123,6 +123,7 @@ function getConfig(ctx, key, fallback = null) {
123
123
  }
124
124
 
125
125
  // src/admin.ts
126
+ import { timingSafeEqual } from "crypto";
126
127
  import { setApiResponse, HTTP } from "@fonderie/core";
127
128
 
128
129
  // src/crypto.ts
@@ -168,6 +169,14 @@ import {
168
169
  } from "@fonderie/store";
169
170
 
170
171
  // src/services/config.ts
172
+ function withParsedValue(row) {
173
+ if (typeof row.value !== "string") return row;
174
+ try {
175
+ return { ...row, value: JSON.parse(row.value) };
176
+ } catch {
177
+ return row;
178
+ }
179
+ }
171
180
  var ENTRY_COLS = `
172
181
  key,
173
182
  value,
@@ -322,11 +331,17 @@ async function deleteSecret(key, environment, store) {
322
331
  function checkAdmin(ctx, adminToken) {
323
332
  const header = ctx.request.headers.get("authorization") ?? "";
324
333
  const token = header.startsWith("Bearer ") ? header.slice(7) : "";
325
- if (!token || token !== adminToken) {
334
+ if (!token || !safeTokenEqual(token, adminToken)) {
326
335
  return setApiResponse(HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
327
336
  }
328
337
  return null;
329
338
  }
339
+ function safeTokenEqual(a, b) {
340
+ const bufA = Buffer.from(a);
341
+ const bufB = Buffer.from(b);
342
+ if (bufA.length !== bufB.length) return false;
343
+ return timingSafeEqual(bufA, bufB);
344
+ }
330
345
  function actorOf(ctx) {
331
346
  return ctx.request.headers.get("x-actor") || "admin-token";
332
347
  }
@@ -346,6 +361,7 @@ function writeOpts(ctx, b) {
346
361
  const env = b["environment"] ?? envOf(ctx);
347
362
  if (env !== void 0) opts.environment = env;
348
363
  if (typeof b["description"] === "string") opts.description = b["description"];
364
+ if (typeof b["active"] === "boolean") opts.active = b["active"];
349
365
  if (typeof b["ifVersion"] === "number") opts.ifVersion = b["ifVersion"];
350
366
  return opts;
351
367
  }
@@ -371,11 +387,11 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
371
387
  // ── config ──────────────────────────────────────────────────
372
388
  ["GET", "/admin/config", g(async (ctx) => {
373
389
  const rows = await listConfigEntries(envOf(ctx) ?? null, store);
374
- return setApiResponse(HTTP.OK, "CONFIG_LISTED", "Config entries", rows);
390
+ return setApiResponse(HTTP.OK, "CONFIG_LISTED", "Config entries", rows.map(withParsedValue));
375
391
  })],
376
392
  ["GET", "/admin/config/:key", g(async (ctx) => {
377
393
  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");
394
+ return row ? setApiResponse(HTTP.OK, "CONFIG_ENTRY", "Config entry", withParsedValue(row)) : setApiResponse(HTTP.NOT_FOUND, "NOT_FOUND", "No such config entry");
379
395
  })],
380
396
  ["PUT", "/admin/config/:key", g(async (ctx) => {
381
397
  const b = body(ctx);
@@ -387,7 +403,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
387
403
  { key: keyOf(ctx), value: b["value"], ...writeOpts(ctx, b) },
388
404
  store
389
405
  );
390
- return setApiResponse(HTTP.OK, "CONFIG_SET", "Config entry saved", row);
406
+ return setApiResponse(HTTP.OK, "CONFIG_SET", "Config entry saved", withParsedValue(row));
391
407
  } catch (err) {
392
408
  return conflictOr(err);
393
409
  }
@@ -398,7 +414,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
398
414
  })],
399
415
  ["GET", "/admin/config/:key/revisions", g(async (ctx) => {
400
416
  const revs = await listConfigRevisions(keyOf(ctx), envOf(ctx) ?? "all", store);
401
- return setApiResponse(HTTP.OK, "REVISIONS", "Config revisions", revs);
417
+ return setApiResponse(HTTP.OK, "REVISIONS", "Config revisions", revs.map(withParsedValue));
402
418
  })],
403
419
  ["POST", "/admin/config/:key/rollback", g(async (ctx) => {
404
420
  const b = body(ctx);
@@ -410,7 +426,7 @@ function buildAdminRoutes(store, adminToken, encryptor = noopEncryptor) {
410
426
  rollbackOpts(ctx, b, toVersion),
411
427
  store
412
428
  );
413
- return setApiResponse(HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
429
+ return setApiResponse(HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, withParsedValue(row));
414
430
  })],
415
431
  // ── secrets (masked) ────────────────────────────────────────
416
432
  ["GET", "/admin/secrets", g(async (ctx) => {
@@ -475,6 +491,8 @@ function conflictOr(err) {
475
491
  }
476
492
 
477
493
  // src/module.ts
494
+ var MIN_ADMIN_TOKEN_LENGTH = 32;
495
+ var PLACEHOLDER_TOKEN = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;
478
496
  var ConfigModule = class {
479
497
  constructor(store, options = {}) {
480
498
  this.store = store;
@@ -499,6 +517,38 @@ var ConfigModule = class {
499
517
  }
500
518
  }
501
519
  }
520
+ // Reported by FonderieApp.checkProductionReadiness. Only the admin surface
521
+ // carries a secret worth guarding; when no adminToken is set the surface
522
+ // isn't registered at all (fail-closed), so there's nothing to flag.
523
+ checkReadiness() {
524
+ const problems = [];
525
+ if (!this.options.secretEncryptor) {
526
+ const inProd = process.env["NODE_ENV"] === "production";
527
+ const secretsExposed = Boolean(this.options.adminToken);
528
+ problems.push({
529
+ module: this.name,
530
+ severity: inProd && secretsExposed ? "error" : "warning",
531
+ message: "no secretEncryptor configured \u2014 secrets are stored plaintext at rest; use createAesGcmEncryptor" + (inProd && secretsExposed ? " (required in production when the secrets admin surface is enabled)" : " for production")
532
+ });
533
+ }
534
+ const token = this.options.adminToken;
535
+ if (token) {
536
+ if (token.length < MIN_ADMIN_TOKEN_LENGTH) {
537
+ problems.push({
538
+ module: this.name,
539
+ severity: "error",
540
+ message: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`
541
+ });
542
+ } else if (PLACEHOLDER_TOKEN.test(token)) {
543
+ problems.push({
544
+ module: this.name,
545
+ severity: "error",
546
+ message: "adminToken looks like a placeholder or dev-default value"
547
+ });
548
+ }
549
+ }
550
+ return problems;
551
+ }
502
552
  };
503
553
  export {
504
554
  CONFIG_MANAGER_KEY,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../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":["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":["VersionConflictError","VersionConflictError"]}
1
+ {"version":3,"sources":["../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":["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 { timingSafeEqual } from 'node:crypto';\n\nimport 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\twithParsedValue,\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 || !safeTokenEqual(token, adminToken)) {\n\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token');\n\t}\n\treturn null;\n}\n\n// Constant-time comparison so a wrong token can't be recovered byte-by-byte\n// from response-timing. Encode to bytes and length-guard first: timingSafeEqual\n// throws on unequal lengths, and that early return is itself acceptable — the\n// secret's length is not the sensitive part.\nfunction safeTokenEqual(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\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; active?: boolean; ifVersion?: number; actor: string } {\n\tconst opts: {\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor: string;\n\t} = {\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\t// Previously dropped here, silently forcing active=true on every HTTP\n\t// write while list reads filter on it.\n\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\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.map(withParsedValue));\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', withParsedValue(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', withParsedValue(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.map(withParsedValue));\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}`, withParsedValue(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\n// The column stores JSON-encoded text; the runtime read path (manager)\n// parses it with a raw-string fallback. The admin surface must serve the\n// same parsed shape — otherwise setConfig({a: 1}) reads back as the string\n// '{\"a\":1}' and the shipped editor re-stringifies it into a degradation\n// loop on every save.\nexport function withParsedValue<T extends { value: unknown }>(row: T): T {\n\tif (typeof row.value !== 'string') return row;\n\ttry {\n\t\treturn { ...row, value: JSON.parse(row.value) };\n\t} catch {\n\t\treturn row;\n\t}\n}\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, IReadinessProblem } 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\n// A weak admin token guards the plaintext-secret reveal surface, so hold it to\n// the same bar as an auth signing secret: 32+ chars and not a copy-pasted\n// placeholder. Mirrors the jwtSecret checks in @fonderie/auth's config-guard.\nconst MIN_ADMIN_TOKEN_LENGTH = 32;\nconst PLACEHOLDER_TOKEN =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|admin-token|min-32-chars/i;\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\t// Reported by FonderieApp.checkProductionReadiness. Only the admin surface\n\t// carries a secret worth guarding; when no adminToken is set the surface\n\t// isn't registered at all (fail-closed), so there's nothing to flag.\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst problems: IReadinessProblem[] = [];\n\t\t// Secrets are stored plaintext at rest unless an encryptor is configured.\n\t\t// It's a hard error in production when the admin secrets surface is enabled\n\t\t// (a plaintext secret is revealable over the API) — this fails the boot gate.\n\t\t// Otherwise (dev, or no admin surface) it's a warning so back-compatible\n\t\t// deployments that never expose secrets aren't broken.\n\t\tif (!this.options.secretEncryptor) {\n\t\t\tconst inProd = process.env['NODE_ENV'] === 'production';\n\t\t\tconst secretsExposed = Boolean(this.options.adminToken);\n\t\t\tproblems.push({\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: inProd && secretsExposed ? 'error' : 'warning',\n\t\t\t\tmessage:\n\t\t\t\t\t'no secretEncryptor configured — secrets are stored plaintext at rest; ' +\n\t\t\t\t\t'use createAesGcmEncryptor' +\n\t\t\t\t\t(inProd && secretsExposed\n\t\t\t\t\t\t? ' (required in production when the secrets admin surface is enabled)'\n\t\t\t\t\t\t: ' for production'),\n\t\t\t});\n\t\t}\n\n\t\tconst token = this.options.adminToken;\n\t\tif (token) {\n\t\t\tif (token.length < MIN_ADMIN_TOKEN_LENGTH) {\n\t\t\t\tproblems.push({\n\t\t\t\t\tmodule: this.name,\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: `adminToken must be at least ${MIN_ADMIN_TOKEN_LENGTH} characters (got ${token.length})`,\n\t\t\t\t});\n\t\t\t} else if (PLACEHOLDER_TOKEN.test(token)) {\n\t\t\t\tproblems.push({\n\t\t\t\t\tmodule: this.name,\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: 'adminToken looks like a placeholder or dev-default value',\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn problems;\n\t}\n}\n"],"mappings":"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","names":["VersionConflictError","VersionConflictError"]}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@fonderie/config",
3
- "version": "4.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "DB-backed feature flags and remote config — per-environment overrides, TTL-based hot reload, and a typed getConfig helper usable inside any middleware.",
5
5
  "keywords": [
6
- "fonderie-js",
6
+ "fonderiejs",
7
7
  "feature-flags",
8
8
  "remote-config",
9
9
  "runtime-config",
@@ -49,19 +49,19 @@
49
49
  "check": "biome check --write src"
50
50
  },
51
51
  "peerDependencies": {
52
- "@fonderie/core": "^0.4.0",
52
+ "@fonderie/core": "^0.5.0",
53
53
  "@fonderie/store": "^0.2.0",
54
54
  "pg": "^8.0.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@fonderie/core": "../core",
58
58
  "@fonderie/store": "../store",
59
- "@types/node": "^25.6.0",
59
+ "@types/node": "^26.4.0",
60
60
  "tsup": "^8.5.1",
61
- "tsx": "^4.21.0",
61
+ "tsx": "^4.23.12",
62
62
  "typescript": "^6.0.3",
63
- "@types/pg": "^8.15.5",
64
- "pg": "^8.13.3"
63
+ "@types/pg": "^8.21.0",
64
+ "pg": "^8.23.0"
65
65
  },
66
66
  "publishConfig": {
67
67
  "access": "public"
@@ -74,11 +74,11 @@
74
74
  ],
75
75
  "repository": {
76
76
  "type": "git",
77
- "url": "git+https://github.com/fonderiejs/sdk.git",
77
+ "url": "git+https://github.com/fonderiejs/fonderie.git",
78
78
  "directory": "packages/config"
79
79
  },
80
- "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/config#readme",
80
+ "homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/config#readme",
81
81
  "bugs": {
82
- "url": "https://github.com/fonderiejs/sdk/issues"
82
+ "url": "https://github.com/fonderiejs/fonderie/issues"
83
83
  }
84
84
  }