@fonderie/config 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/manager.ts","../src/middlewares/config-context.ts","../src/module.ts","../src/services/config.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: 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}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\n\nimport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nimport { configContextMiddleware } from './middlewares/config-context';\n\nexport class ConfigModule implements IFonderieModule {\n\treadonly name = '@fonderie/config';\n\treadonly manager: RemoteConfigManager;\n\n\tconstructor(store: IStoreAdapter, options: IConfigOptions = {}) {\n\t\tthis.manager = new RemoteConfigManager(store, options);\n\t}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait this.manager.boot();\n\t\tapp.use(configContextMiddleware(this.manager));\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigEntry } from '../types';\n\nconst SELECT_ENTRY = `\n\tSELECT\n\t\tkey,\n\t\tvalue,\n\t\tenvironment,\n\t\tdescription,\n\t\tactive,\n\t\tupdated_at AS \"updatedAt\"\n\tFROM fonderie_config`;\n\nexport async function listConfigEntries(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry[]> {\n\treturn environment\n\t\t? store.query<IConfigEntry>(\n\t\t\t\t`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<IConfigEntry>(`${SELECT_ENTRY} ORDER BY environment, key`);\n}\n\nexport async function getConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry | null> {\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\nexport async function setConfigEntry(\n\topts: {\n\t\tkey: string;\n\t\tvalue: unknown;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\tconst env = opts.environment ?? 'all';\n\tconst raw = typeof opts.value === 'string' ? opts.value : JSON.stringify(opts.value);\n\tconst active = opts.active ?? true;\n\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`INSERT INTO fonderie_config (key, value, environment, description, active)\n\t\tVALUES ($1, $2, $3, $4, $5)\n\t\tON CONFLICT (key, environment) DO UPDATE SET\n\t\t\tvalue = $2,\n\t\t\tdescription = COALESCE($4, fonderie_config.description),\n\t\t\tactive = $5,\n\t\t\tupdated_at = now()\n\t\tRETURNING\n\t\t\tkey, value, environment, description, active,\n\t\t\tupdated_at AS \"updatedAt\"`,\n\t\t[opts.key, raw, env, opts.description ?? null, active],\n\t);\n\tif (!row) throw new Error('Failed to upsert config entry');\n\treturn row;\n}\n\nexport async function deleteConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/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,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/middlewares/index.ts
@@ -26,6 +36,7 @@ __export(middlewares_exports, {
26
36
  module.exports = __toCommonJS(middlewares_exports);
27
37
 
28
38
  // src/manager.ts
39
+ var import_pg = __toESM(require("pg"), 1);
29
40
  var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
30
41
  var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
31
42
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middlewares/index.ts","../../src/manager.ts","../../src/middlewares/config-context.ts"],"sourcesContent":["export { configContextMiddleware, getConfig } from './config-context';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: 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}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,sBAAsB;AAwGrB,IAAM,qBAAqB;;;ACpG3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
1
+ {"version":3,"sources":["../../src/middlewares/index.ts","../../src/manager.ts","../../src/middlewares/config-context.ts"],"sourcesContent":["export { configContextMiddleware, getConfig } from './config-context';\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,gBAAe;AAOf,IAAM,sBAAsB;AAmIrB,IAAM,qBAAqB;;;ACjI3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
@@ -1,3 +1,3 @@
1
- export { c as configContextMiddleware, g as getConfig } from '../index-B2jOiL09.cjs';
1
+ export { c as configContextMiddleware, g as getConfig } from '../index-CLaJRR69.cjs';
2
2
  import '@fonderie/core';
3
3
  import '@fonderie/store';
@@ -1,3 +1,3 @@
1
- export { c as configContextMiddleware, g as getConfig } from '../index-B2jOiL09.js';
1
+ export { c as configContextMiddleware, g as getConfig } from '../index-CLaJRR69.js';
2
2
  import '@fonderie/core';
3
3
  import '@fonderie/store';
@@ -1,4 +1,5 @@
1
1
  // src/manager.ts
2
+ import pg from "pg";
2
3
  var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
3
4
  var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
4
5
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/manager.ts","../../src/middlewares/config-context.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: 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}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n"],"mappings":";AAKA,IAAM,sBAAsB;AAwGrB,IAAM,qBAAqB;;;ACpG3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
1
+ {"version":3,"sources":["../../src/manager.ts","../../src/middlewares/config-context.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"],"mappings":";AAAA,OAAO,QAAQ;AAOf,IAAM,sBAAsB;AAmIrB,IAAM,qBAAqB;;;ACjI3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
@@ -0,0 +1,20 @@
1
+ -- Version index + revision history for config entries (optimistic concurrency +
2
+ -- rollback). Each committed write bumps `version` and appends a revision row;
3
+ -- history is append-only and never mutated. `updated_by` records the actor.
4
+
5
+ ALTER TABLE fonderie_config
6
+ ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1,
7
+ ADD COLUMN IF NOT EXISTS updated_by TEXT;
8
+
9
+ CREATE TABLE IF NOT EXISTS fonderie_config_revisions (
10
+ key TEXT NOT NULL,
11
+ environment TEXT NOT NULL DEFAULT 'all',
12
+ value TEXT NOT NULL,
13
+ version INT NOT NULL,
14
+ actor TEXT,
15
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
16
+ PRIMARY KEY (key, environment, version)
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS fonderie_config_rev_key_env_idx
20
+ ON fonderie_config_revisions (key, environment, version DESC);
@@ -0,0 +1,32 @@
1
+ -- Secrets: the ConfigMap-vs-Secret split (k8s). Same lifecycle machinery as
2
+ -- config (version index, revisions, rollback, push propagation) but a SEPARATE
3
+ -- table + read path so a masking bug in config can't leak secrets. `value`
4
+ -- holds ciphertext when an encryptor is configured (plaintext under the no-op
5
+ -- default). Values are never returned by the masked admin reads — only the
6
+ -- explicit reveal path decrypts.
7
+
8
+ CREATE TABLE IF NOT EXISTS fonderie_secrets (
9
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
10
+ key TEXT NOT NULL,
11
+ value TEXT NOT NULL,
12
+ environment TEXT NOT NULL DEFAULT 'all',
13
+ description TEXT,
14
+ active BOOLEAN NOT NULL DEFAULT true,
15
+ version INT NOT NULL DEFAULT 1,
16
+ updated_by TEXT,
17
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
18
+ UNIQUE (key, environment)
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS fonderie_secret_revisions (
22
+ key TEXT NOT NULL,
23
+ environment TEXT NOT NULL DEFAULT 'all',
24
+ value TEXT NOT NULL,
25
+ version INT NOT NULL,
26
+ actor TEXT,
27
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
28
+ PRIMARY KEY (key, environment, version)
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS fonderie_secret_rev_key_env_idx
32
+ ON fonderie_secret_revisions (key, environment, version DESC);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface IConfigEntry {\n\tkey: string;\n\tvalue: unknown;\n\tenvironment: string;\n\tdescription: string | null;\n\tactive: boolean;\n\tupdatedAt: string;\n}\n\nexport interface IConfigSnapshot {\n\tentries: Record<string, unknown>;\n\tfetchedAt: Date;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface IConfigEntry {\n\tkey: string;\n\tvalue: unknown;\n\tenvironment: string;\n\tdescription: string | null;\n\tactive: boolean;\n\t// Monotonic version index — the compare-and-swap token for optimistic\n\t// concurrency and the anchor for revisions/rollback.\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\n// One immutable point in a config entry's history (append-only).\nexport interface IConfigRevision {\n\tkey: string;\n\tenvironment: string;\n\tvalue: unknown;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nexport interface IConfigSnapshot {\n\tentries: Record<string, unknown>;\n\tfetchedAt: Date;\n}\n\n// A secret's metadata — deliberately **without** the value. Admin list/get\n// return this; only the explicit reveal path returns the decrypted value.\nexport interface ISecretEntry {\n\tkey: string;\n\tenvironment: string;\n\tdescription: string | null;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\n// A secret revision's metadata (no value).\nexport interface ISecretRevision {\n\tkey: string;\n\tenvironment: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/dist/types.d.cts CHANGED
@@ -4,11 +4,37 @@ interface IConfigEntry {
4
4
  environment: string;
5
5
  description: string | null;
6
6
  active: boolean;
7
+ version: number;
8
+ updatedBy: string | null;
7
9
  updatedAt: string;
8
10
  }
11
+ interface IConfigRevision {
12
+ key: string;
13
+ environment: string;
14
+ value: unknown;
15
+ version: number;
16
+ actor: string | null;
17
+ createdAt: string;
18
+ }
9
19
  interface IConfigSnapshot {
10
20
  entries: Record<string, unknown>;
11
21
  fetchedAt: Date;
12
22
  }
23
+ interface ISecretEntry {
24
+ key: string;
25
+ environment: string;
26
+ description: string | null;
27
+ active: boolean;
28
+ version: number;
29
+ updatedBy: string | null;
30
+ updatedAt: string;
31
+ }
32
+ interface ISecretRevision {
33
+ key: string;
34
+ environment: string;
35
+ version: number;
36
+ actor: string | null;
37
+ createdAt: string;
38
+ }
13
39
 
14
- export type { IConfigEntry, IConfigSnapshot };
40
+ export type { IConfigEntry, IConfigRevision, IConfigSnapshot, ISecretEntry, ISecretRevision };
package/dist/types.d.ts CHANGED
@@ -4,11 +4,37 @@ interface IConfigEntry {
4
4
  environment: string;
5
5
  description: string | null;
6
6
  active: boolean;
7
+ version: number;
8
+ updatedBy: string | null;
7
9
  updatedAt: string;
8
10
  }
11
+ interface IConfigRevision {
12
+ key: string;
13
+ environment: string;
14
+ value: unknown;
15
+ version: number;
16
+ actor: string | null;
17
+ createdAt: string;
18
+ }
9
19
  interface IConfigSnapshot {
10
20
  entries: Record<string, unknown>;
11
21
  fetchedAt: Date;
12
22
  }
23
+ interface ISecretEntry {
24
+ key: string;
25
+ environment: string;
26
+ description: string | null;
27
+ active: boolean;
28
+ version: number;
29
+ updatedBy: string | null;
30
+ updatedAt: string;
31
+ }
32
+ interface ISecretRevision {
33
+ key: string;
34
+ environment: string;
35
+ version: number;
36
+ actor: string | null;
37
+ createdAt: string;
38
+ }
13
39
 
14
- export type { IConfigEntry, IConfigSnapshot };
40
+ export type { IConfigEntry, IConfigRevision, IConfigSnapshot, ISecretEntry, ISecretRevision };
package/package.json CHANGED
@@ -1,81 +1,84 @@
1
1
  {
2
- "name": "@fonderie/config",
3
- "version": "3.0.0",
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
- "keywords": [
6
- "fonderie-js",
7
- "feature-flags",
8
- "remote-config",
9
- "runtime-config",
10
- "saas",
11
- "typescript"
12
- ],
13
- "license": "MIT",
14
- "type": "module",
15
- "engines": {
16
- "node": ">=20"
17
- },
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js",
22
- "require": "./dist/index.cjs"
23
- },
24
- "./types": {
25
- "types": "./dist/types.d.ts",
26
- "import": "./dist/types.js",
27
- "require": "./dist/types.cjs"
28
- },
29
- "./middleware": {
30
- "types": "./dist/middlewares/index.d.ts",
31
- "import": "./dist/middlewares/index.js",
32
- "require": "./dist/middlewares/index.cjs"
33
- },
34
- "./migrations": {
35
- "types": "./dist/migrations/index.d.ts",
36
- "import": "./dist/migrations/index.js"
37
- }
38
- },
39
- "main": "./dist/index.cjs",
40
- "module": "./dist/index.js",
41
- "types": "./dist/index.d.ts",
42
- "scripts": {
43
- "build": "tsup && tsup --config tsup.migrations.ts",
44
- "dev": "tsup --watch",
45
- "typecheck": "tsc --noEmit",
46
- "test": "tsx --test src/__tests__/*.test.ts",
47
- "lint": "biome lint src",
48
- "format": "biome format --write src",
49
- "check": "biome check --write src"
50
- },
51
- "peerDependencies": {
52
- "@fonderie/core": "^0.3.0",
53
- "@fonderie/store": "^0.1.1"
54
- },
55
- "devDependencies": {
56
- "@fonderie/core": "../core",
57
- "@fonderie/store": "../store",
58
- "@types/node": "^25.6.0",
59
- "tsup": "^8.5.1",
60
- "tsx": "^4.21.0",
61
- "typescript": "^6.0.3"
62
- },
63
- "publishConfig": {
64
- "access": "public"
65
- },
66
- "files": [
67
- "dist",
68
- "brain",
69
- "LICENSE",
70
- "README.md"
71
- ],
72
- "repository": {
73
- "type": "git",
74
- "url": "git+https://github.com/fonderiejs/sdk.git",
75
- "directory": "packages/config"
76
- },
77
- "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/config#readme",
78
- "bugs": {
79
- "url": "https://github.com/fonderiejs/sdk/issues"
80
- }
2
+ "name": "@fonderie/config",
3
+ "version": "4.0.0",
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
+ "keywords": [
6
+ "fonderie-js",
7
+ "feature-flags",
8
+ "remote-config",
9
+ "runtime-config",
10
+ "saas",
11
+ "typescript"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./types": {
25
+ "types": "./dist/types.d.ts",
26
+ "import": "./dist/types.js",
27
+ "require": "./dist/types.cjs"
28
+ },
29
+ "./middleware": {
30
+ "types": "./dist/middlewares/index.d.ts",
31
+ "import": "./dist/middlewares/index.js",
32
+ "require": "./dist/middlewares/index.cjs"
33
+ },
34
+ "./migrations": {
35
+ "types": "./dist/migrations/index.d.ts",
36
+ "import": "./dist/migrations/index.js"
37
+ }
38
+ },
39
+ "main": "./dist/index.cjs",
40
+ "module": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "scripts": {
43
+ "build": "tsup && tsup --config tsup.migrations.ts",
44
+ "dev": "tsup --watch",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "tsx --test src/__tests__/*.test.ts",
47
+ "lint": "biome lint src",
48
+ "format": "biome format --write src",
49
+ "check": "biome check --write src"
50
+ },
51
+ "peerDependencies": {
52
+ "@fonderie/core": "^0.4.0",
53
+ "@fonderie/store": "^0.2.0",
54
+ "pg": "^8.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@fonderie/core": "../core",
58
+ "@fonderie/store": "../store",
59
+ "@types/node": "^25.6.0",
60
+ "tsup": "^8.5.1",
61
+ "tsx": "^4.21.0",
62
+ "typescript": "^6.0.3",
63
+ "@types/pg": "^8.15.5",
64
+ "pg": "^8.13.3"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "files": [
70
+ "dist",
71
+ "brain",
72
+ "LICENSE",
73
+ "README.md"
74
+ ],
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/fonderiejs/sdk.git",
78
+ "directory": "packages/config"
79
+ },
80
+ "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/config#readme",
81
+ "bugs": {
82
+ "url": "https://github.com/fonderiejs/sdk/issues"
83
+ }
81
84
  }