@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 +2 -2
- package/brain/outcomes.md +5 -5
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +56 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +56 -6
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
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/
|
|
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
|
-
[
|
|
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'); })` |
|
package/brain/signatures.md
CHANGED
|
@@ -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
|
|
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 = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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": "
|
|
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
|
-
"
|
|
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.
|
|
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": "^
|
|
59
|
+
"@types/node": "^26.4.0",
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
|
-
"tsx": "^4.
|
|
61
|
+
"tsx": "^4.23.12",
|
|
62
62
|
"typescript": "^6.0.3",
|
|
63
|
-
"@types/pg": "^8.
|
|
64
|
-
"pg": "^8.
|
|
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/
|
|
77
|
+
"url": "git+https://github.com/fonderiejs/fonderie.git",
|
|
78
78
|
"directory": "packages/config"
|
|
79
79
|
},
|
|
80
|
-
"homepage": "https://github.com/fonderiejs/
|
|
80
|
+
"homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/config#readme",
|
|
81
81
|
"bugs": {
|
|
82
|
-
"url": "https://github.com/fonderiejs/
|
|
82
|
+
"url": "https://github.com/fonderiejs/fonderie/issues"
|
|
83
83
|
}
|
|
84
84
|
}
|