enpilink 1.0.2 → 1.0.4
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/server/analytics.d.ts +61 -21
- package/dist/server/analytics.js +107 -52
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/analytics.test.js +63 -12
- package/dist/server/analytics.test.js.map +1 -1
- package/dist/server/capture-gate.d.ts +47 -0
- package/dist/server/capture-gate.js +39 -0
- package/dist/server/capture-gate.js.map +1 -0
- package/dist/server/capture-gate.test.d.ts +1 -0
- package/dist/server/capture-gate.test.js +124 -0
- package/dist/server/capture-gate.test.js.map +1 -0
- package/dist/server/config/config.test.js +201 -8
- package/dist/server/config/config.test.js.map +1 -1
- package/dist/server/config/index.d.ts +3 -2
- package/dist/server/config/index.js +3 -2
- package/dist/server/config/index.js.map +1 -1
- package/dist/server/config/presets.d.ts +36 -0
- package/dist/server/config/presets.js +46 -0
- package/dist/server/config/presets.js.map +1 -0
- package/dist/server/config/resolve.d.ts +42 -3
- package/dist/server/config/resolve.js +88 -8
- package/dist/server/config/resolve.js.map +1 -1
- package/dist/server/config/router.d.ts +22 -14
- package/dist/server/config/router.js +163 -51
- package/dist/server/config/router.js.map +1 -1
- package/dist/server/config/schema.d.ts +39 -1
- package/dist/server/config/schema.js +121 -0
- package/dist/server/config/schema.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/storage/memory.d.ts +1 -0
- package/dist/server/storage/memory.js +14 -0
- package/dist/server/storage/memory.js.map +1 -1
- package/dist/server/storage/memory.test.js +17 -0
- package/dist/server/storage/memory.test.js.map +1 -1
- package/dist/server/storage/postgres.d.ts +1 -0
- package/dist/server/storage/postgres.js +12 -0
- package/dist/server/storage/postgres.js.map +1 -1
- package/dist/server/storage/postgres.test.js +17 -0
- package/dist/server/storage/postgres.test.js.map +1 -1
- package/dist/server/storage/sqlite.d.ts +1 -0
- package/dist/server/storage/sqlite.js +21 -0
- package/dist/server/storage/sqlite.js.map +1 -1
- package/dist/server/storage/sqlite.test.js +17 -0
- package/dist/server/storage/sqlite.test.js.map +1 -1
- package/dist/server/storage/types.d.ts +6 -0
- package/dist/server/storage/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../../src/server/config/config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport express from \"express\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { MemoryStorageAdapter } from \"../storage/memory.js\";\nimport type { StorageAdapter } from \"../storage/types.js\";\nimport { MASKED, resolveConfig, validateRuntimeWrite } from \"./resolve.js\";\nimport { createConfigRouter } from \"./router.js\";\n\n/** A throwaway dir with no enpilink.config.* so file source never interferes. */\nlet cwd: string;\n\nbeforeEach(() => {\n cwd = fs.mkdtempSync(path.join(os.tmpdir(), \"enpi-cfg-\"));\n});\nafterEach(() => {\n fs.rmSync(cwd, { recursive: true, force: true });\n // Clean any env we set.\n for (const k of Object.keys(process.env)) {\n if (k.startsWith(\"ENPILINK_CFG_\") || k === \"ENPILINK_ANALYTICS\") {\n delete process.env[k];\n }\n }\n});\n\ndescribe(\"resolveConfig precedence\", () => {\n it(\"defaults when no source supplies a value\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.value).toBe(1);\n expect(s?.source).toBe(\"default\");\n expect(s?.envLocked).toBe(false);\n });\n\n it(\"db beats default for runtime keys\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25, \"tester\");\n const { settings, values } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"db\");\n expect(s?.value).toBe(0.25);\n expect(values[\"analytics.sampleRate\"]).toBe(0.25);\n });\n\n it(\"file beats db\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25);\n fs.writeFileSync(\n path.join(cwd, \"enpilink.config.json\"),\n JSON.stringify({ \"analytics.sampleRate\": 0.5 }),\n );\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"file\");\n expect(s?.value).toBe(0.5);\n expect(s?.envLocked).toBe(true); // file pins it → read-only in UI\n });\n\n it(\"env beats file and db\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25);\n fs.writeFileSync(\n path.join(cwd, \"enpilink.config.json\"),\n JSON.stringify({ \"analytics.sampleRate\": 0.5 }),\n );\n process.env.ENPILINK_CFG_ANALYTICS_SAMPLE_RATE = \"0.75\";\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"env\");\n expect(s?.value).toBe(0.75);\n expect(s?.envLocked).toBe(true);\n });\n\n it(\"coerces env booleans/numbers to typed values\", async () => {\n process.env.ENPILINK_CFG_FLAGS_LIVE_LOGS = \"false\";\n process.env.ENPILINK_CFG_RETENTION_EVENTS = \"1234\";\n const { values } = await resolveConfig(null, cwd);\n expect(values[\"flags.liveLogs\"]).toBe(false);\n expect(values[\"retention.events\"]).toBe(1234);\n });\n});\n\ndescribe(\"secret masking\", () => {\n it(\"never returns a secret value in plaintext\", async () => {\n process.env.ENPILINK_ADMIN_TOKEN = \"super-secret-xyz\";\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"adminAuthToken\");\n expect(s?.secret).toBe(true);\n expect(s?.envLocked).toBe(true);\n expect(s?.value).toBe(MASKED);\n expect(JSON.stringify(settings)).not.toContain(\"super-secret-xyz\");\n delete process.env.ENPILINK_ADMIN_TOKEN;\n });\n\n it(\"unset secret reports null, not masked\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"adminAuthToken\");\n expect(s?.value).toBeNull();\n });\n\n it(\"bootstrap keys are always env-locked\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n for (const key of [\"storage\", \"dbPath\", \"port\", \"admin\"]) {\n const s = settings.find((x) => x.key === key);\n expect(s?.tier).toBe(\"bootstrap\");\n expect(s?.envLocked).toBe(true);\n }\n });\n});\n\ndescribe(\"validateRuntimeWrite\", () => {\n it(\"accepts a valid runtime value\", () => {\n const r = validateRuntimeWrite(\"analytics.sampleRate\", 0.3);\n expect(r.ok).toBe(true);\n if (r.ok) {\n expect(r.value).toBe(0.3);\n }\n });\n it(\"coerces a stringy boolean for a flag\", () => {\n const r = validateRuntimeWrite(\"flags.liveLogs\", \"true\");\n expect(r.ok).toBe(true);\n if (r.ok) {\n expect(r.value).toBe(true);\n }\n });\n it(\"rejects out-of-range\", () => {\n const r = validateRuntimeWrite(\"analytics.sampleRate\", 5);\n expect(r.ok).toBe(false);\n });\n it(\"rejects bootstrap key\", () => {\n expect(validateRuntimeWrite(\"port\", 8080).ok).toBe(false);\n });\n it(\"rejects secret key\", () => {\n expect(validateRuntimeWrite(\"adminAuthToken\", \"x\").ok).toBe(false);\n });\n});\n\n// --- Router integration tests ---\n\nasync function request(\n app: express.Express,\n method: string,\n url: string,\n body?: unknown,\n): Promise<{ status: number; json: unknown }> {\n const { createServer } = await import(\"node:http\");\n const server = createServer(app);\n await new Promise<void>((r) => server.listen(0, r));\n const addr = server.address();\n const port = typeof addr === \"object\" && addr ? addr.port : 0;\n try {\n const res = await fetch(`http://127.0.0.1:${port}${url}`, {\n method,\n headers: body ? { \"content-type\": \"application/json\" } : undefined,\n body: body ? JSON.stringify(body) : undefined,\n });\n return { status: res.status, json: await res.json().catch(() => null) };\n } finally {\n await new Promise<void>((r) => server.close(() => r()));\n }\n}\n\nfunction appWith(storage: StorageAdapter | null): express.Express {\n const app = express();\n app.use(express.json());\n app.use(createConfigRouter(() => storage));\n return app;\n}\n\ndescribe(\"config router\", () => {\n it(\"GET /config returns settings; never 500 with no storage\", async () => {\n const app = appWith(null);\n const { status, json } = await request(app, \"GET\", \"/__enpilink/config\");\n expect(status).toBe(200);\n const settings = (json as { settings: unknown[] }).settings;\n expect(Array.isArray(settings)).toBe(true);\n expect(settings.length).toBeGreaterThan(0);\n });\n\n it(\"PUT a runtime key persists + writes an audit row\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/analytics.sampleRate\",\n { value: 0.4 },\n );\n expect(status).toBe(200);\n expect((json as { ok: boolean }).ok).toBe(true);\n expect(await storage.getConfig(\"analytics.sampleRate\")).toBe(0.4);\n const audit = await storage.getConfigAudit();\n expect(audit[0]).toMatchObject({\n key: \"analytics.sampleRate\",\n newValue: 0.4,\n });\n });\n\n it(\"PUT rejects a bootstrap key (403)\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/port\", {\n value: 9999,\n });\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects a secret key (403)\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/adminAuthToken\",\n { value: \"x\" },\n );\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects an unknown key (404)\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/nope\", {\n value: 1,\n });\n expect(status).toBe(404);\n });\n\n it(\"PUT rejects an out-of-range runtime value (400)\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/analytics.sampleRate\",\n { value: 99 },\n );\n expect(status).toBe(400);\n });\n\n it(\"PUT with no storage → 409 (nowhere to persist)\", async () => {\n const app = appWith(null);\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/flags.liveLogs\",\n { value: false },\n );\n expect(status).toBe(409);\n });\n\n it(\"GET /config/audit surfaces entries\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"flags.liveLogs\", false, \"tester\");\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"GET\",\n \"/__enpilink/config/audit\",\n );\n expect(status).toBe(200);\n const audit = (json as { audit: Array<{ key: string }> }).audit;\n expect(audit[0]?.key).toBe(\"flags.liveLogs\");\n });\n\n it(\"GET /config/audit with no storage → empty, never 500\", async () => {\n const app = appWith(null);\n const { status, json } = await request(\n app,\n \"GET\",\n \"/__enpilink/config/audit\",\n );\n expect(status).toBe(200);\n expect((json as { audit: unknown[] }).audit).toEqual([]);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../../src/server/config/config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport express from \"express\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { MemoryStorageAdapter } from \"../storage/memory.js\";\nimport type { StorageAdapter } from \"../storage/types.js\";\nimport {\n MASKED,\n resetBootSnapshotForTests,\n resolveConfig,\n validateConfigWrite,\n validateRuntimeWrite,\n} from \"./resolve.js\";\nimport { createConfigRouter } from \"./router.js\";\n\n/** A throwaway dir with no enpilink.config.* so file source never interferes. */\nlet cwd: string;\n\nconst RESTART_ENV = [\"ENPILINK_STORAGE\", \"ENPILINK_DB_PATH\", \"PORT\"];\n\nbeforeEach(() => {\n cwd = fs.mkdtempSync(path.join(os.tmpdir(), \"enpi-cfg-\"));\n resetBootSnapshotForTests();\n});\nafterEach(() => {\n fs.rmSync(cwd, { recursive: true, force: true });\n // Clean any env we set.\n for (const k of Object.keys(process.env)) {\n if (\n k.startsWith(\"ENPILINK_CFG_\") ||\n k === \"ENPILINK_ANALYTICS\" ||\n k === \"ENPILINK_ADMIN_TOKEN\" ||\n k === \"ENPILINK_ADMIN\" ||\n RESTART_ENV.includes(k)\n ) {\n delete process.env[k];\n }\n }\n resetBootSnapshotForTests();\n});\n\ndescribe(\"resolveConfig precedence\", () => {\n it(\"defaults when no source supplies a value\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.value).toBe(1);\n expect(s?.source).toBe(\"default\");\n expect(s?.envLocked).toBe(false);\n });\n\n it(\"db beats default for runtime keys\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25, \"tester\");\n const { settings, values } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"db\");\n expect(s?.value).toBe(0.25);\n expect(values[\"analytics.sampleRate\"]).toBe(0.25);\n });\n\n it(\"file beats db\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25);\n fs.writeFileSync(\n path.join(cwd, \"enpilink.config.json\"),\n JSON.stringify({ \"analytics.sampleRate\": 0.5 }),\n );\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"file\");\n expect(s?.value).toBe(0.5);\n expect(s?.envLocked).toBe(true); // file pins it → read-only in UI\n });\n\n it(\"env beats file and db\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"analytics.sampleRate\", 0.25);\n fs.writeFileSync(\n path.join(cwd, \"enpilink.config.json\"),\n JSON.stringify({ \"analytics.sampleRate\": 0.5 }),\n );\n process.env.ENPILINK_CFG_ANALYTICS_SAMPLE_RATE = \"0.75\";\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(s?.source).toBe(\"env\");\n expect(s?.value).toBe(0.75);\n expect(s?.envLocked).toBe(true);\n });\n\n it(\"coerces env booleans/numbers to typed values\", async () => {\n process.env.ENPILINK_CFG_FLAGS_LIVE_LOGS = \"false\";\n process.env.ENPILINK_CFG_RETENTION_EVENTS = \"1234\";\n const { values } = await resolveConfig(null, cwd);\n expect(values[\"flags.liveLogs\"]).toBe(false);\n expect(values[\"retention.events\"]).toBe(1234);\n });\n});\n\ndescribe(\"secret masking\", () => {\n it(\"never returns a secret value in plaintext\", async () => {\n process.env.ENPILINK_ADMIN_TOKEN = \"super-secret-xyz\";\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"adminAuthToken\");\n expect(s?.secret).toBe(true);\n expect(s?.envLocked).toBe(true);\n expect(s?.value).toBe(MASKED);\n expect(JSON.stringify(settings)).not.toContain(\"super-secret-xyz\");\n delete process.env.ENPILINK_ADMIN_TOKEN;\n });\n\n it(\"unset secret reports null, not masked\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"adminAuthToken\");\n expect(s?.value).toBeNull();\n });\n\n it(\"readonly bootstrap key (admin) is always env-locked\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const s = settings.find((x) => x.key === \"admin\");\n expect(s?.tier).toBe(\"bootstrap\");\n expect(s?.editable).toBe(\"readonly\");\n expect(s?.envLocked).toBe(true);\n });\n\n it(\"restart-tier bootstrap keys are editable when not env/file-pinned\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n for (const key of [\"storage\", \"dbPath\", \"port\"]) {\n const s = settings.find((x) => x.key === key);\n expect(s?.tier).toBe(\"bootstrap\");\n expect(s?.editable).toBe(\"restart\");\n expect(s?.envLocked).toBe(false);\n }\n });\n});\n\ndescribe(\"validateRuntimeWrite\", () => {\n it(\"accepts a valid runtime value\", () => {\n const r = validateRuntimeWrite(\"analytics.sampleRate\", 0.3);\n expect(r.ok).toBe(true);\n if (r.ok) {\n expect(r.value).toBe(0.3);\n }\n });\n it(\"coerces a stringy boolean for a flag\", () => {\n const r = validateRuntimeWrite(\"flags.liveLogs\", \"true\");\n expect(r.ok).toBe(true);\n if (r.ok) {\n expect(r.value).toBe(true);\n }\n });\n it(\"rejects out-of-range\", () => {\n const r = validateRuntimeWrite(\"analytics.sampleRate\", 5);\n expect(r.ok).toBe(false);\n });\n it(\"rejects bootstrap key\", () => {\n expect(validateRuntimeWrite(\"port\", 8080).ok).toBe(false);\n });\n it(\"rejects secret key\", () => {\n expect(validateRuntimeWrite(\"adminAuthToken\", \"x\").ok).toBe(false);\n });\n});\n\n// --- Router integration tests ---\n\nasync function request(\n app: express.Express,\n method: string,\n url: string,\n body?: unknown,\n): Promise<{ status: number; json: unknown }> {\n const { createServer } = await import(\"node:http\");\n const server = createServer(app);\n await new Promise<void>((r) => server.listen(0, r));\n const addr = server.address();\n const port = typeof addr === \"object\" && addr ? addr.port : 0;\n try {\n const res = await fetch(`http://127.0.0.1:${port}${url}`, {\n method,\n headers: body ? { \"content-type\": \"application/json\" } : undefined,\n body: body ? JSON.stringify(body) : undefined,\n });\n return { status: res.status, json: await res.json().catch(() => null) };\n } finally {\n await new Promise<void>((r) => server.close(() => r()));\n }\n}\n\nfunction appWith(storage: StorageAdapter | null): express.Express {\n const app = express();\n app.use(express.json());\n app.use(createConfigRouter(() => storage));\n return app;\n}\n\ndescribe(\"config router\", () => {\n it(\"GET /config returns settings; never 500 with no storage\", async () => {\n const app = appWith(null);\n const { status, json } = await request(app, \"GET\", \"/__enpilink/config\");\n expect(status).toBe(200);\n const settings = (json as { settings: unknown[] }).settings;\n expect(Array.isArray(settings)).toBe(true);\n expect(settings.length).toBeGreaterThan(0);\n });\n\n it(\"PUT a runtime key persists + writes an audit row\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/analytics.sampleRate\",\n { value: 0.4 },\n );\n expect(status).toBe(200);\n expect((json as { ok: boolean }).ok).toBe(true);\n expect(await storage.getConfig(\"analytics.sampleRate\")).toBe(0.4);\n const audit = await storage.getConfigAudit();\n expect(audit[0]).toMatchObject({\n key: \"analytics.sampleRate\",\n newValue: 0.4,\n });\n });\n\n it(\"PUT rejects a readonly bootstrap key like `admin` (403)\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/admin\", {\n value: true,\n });\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects a secret key (403)\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/adminAuthToken\",\n { value: \"x\" },\n );\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects an unknown key (404)\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/nope\", {\n value: 1,\n });\n expect(status).toBe(404);\n });\n\n it(\"PUT rejects an out-of-range runtime value (400)\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/analytics.sampleRate\",\n { value: 99 },\n );\n expect(status).toBe(400);\n });\n\n it(\"PUT with no storage → 409 (nowhere to persist)\", async () => {\n const app = appWith(null);\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/flags.liveLogs\",\n { value: false },\n );\n expect(status).toBe(409);\n });\n\n it(\"GET /config/audit surfaces entries\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"flags.liveLogs\", false, \"tester\");\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"GET\",\n \"/__enpilink/config/audit\",\n );\n expect(status).toBe(200);\n const audit = (json as { audit: Array<{ key: string }> }).audit;\n expect(audit[0]?.key).toBe(\"flags.liveLogs\");\n });\n\n it(\"GET /config/audit with no storage → empty, never 500\", async () => {\n const app = appWith(null);\n const { status, json } = await request(\n app,\n \"GET\",\n \"/__enpilink/config/audit\",\n );\n expect(status).toBe(200);\n expect((json as { audit: unknown[] }).audit).toEqual([]);\n });\n});\n\n// --- Per-key metadata (richer ResolvedSetting) ---\n\ndescribe(\"resolved setting metadata\", () => {\n it(\"exposes label/description/group/unit/default/editable per key\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const sample = settings.find((s) => s.key === \"analytics.sampleRate\");\n expect(sample?.label).toBe(\"Sampling rate\");\n expect(typeof sample?.description).toBe(\"string\");\n expect(sample?.group).toBe(\"Analytics\");\n expect(sample?.unit).toBe(\"0–1 ratio\");\n expect(sample?.default).toBe(1);\n expect(sample?.editable).toBe(\"runtime\");\n });\n\n it(\"classifies restart vs readonly editability\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const byKey = new Map(settings.map((s) => [s.key, s]));\n expect(byKey.get(\"port\")?.editable).toBe(\"restart\");\n expect(byKey.get(\"storage\")?.editable).toBe(\"restart\");\n expect(byKey.get(\"dbPath\")?.editable).toBe(\"restart\");\n expect(byKey.get(\"admin\")?.editable).toBe(\"readonly\");\n expect(byKey.get(\"adminAuthToken\")?.editable).toBe(\"readonly\");\n });\n\n it(\"restart-tier keys NOT pinned by env/file are not env-locked\", async () => {\n const { settings } = await resolveConfig(null, cwd);\n const port = settings.find((s) => s.key === \"port\");\n expect(port?.envLocked).toBe(false);\n const admin = settings.find((s) => s.key === \"admin\");\n // readonly key stays env-locked (read-only) even without an env pin.\n expect(admin?.envLocked).toBe(true);\n });\n\n it(\"modified=true only when a DB override differs from the default\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"retention.events\", 9999);\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"retention.events\");\n expect(s?.modified).toBe(true);\n // an untouched key is not modified\n const other = settings.find((x) => x.key === \"analytics.sampleRate\");\n expect(other?.modified).toBe(false);\n });\n});\n\n// --- Restart-tier writes + restartRequired ---\n\ndescribe(\"restart-tier editability\", () => {\n it(\"validateConfigWrite accepts a restart key, rejects secret\", () => {\n expect(validateConfigWrite(\"port\", 8080).ok).toBe(true);\n expect(validateConfigWrite(\"storage\", \"sqlite\").ok).toBe(true);\n expect(validateConfigWrite(\"adminAuthToken\", \"x\").ok).toBe(false);\n expect(validateConfigWrite(\"admin\", true).ok).toBe(false);\n });\n\n it(\"PUT a restart key persists + flags restartRequired\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/port\",\n {\n value: 8080,\n },\n );\n expect(status).toBe(200);\n expect((json as { restartRequired: boolean }).restartRequired).toBe(true);\n expect(await storage.getConfig(\"port\")).toBe(8080);\n\n // The boot snapshot is the default 3000; the persisted 8080 differs → pending.\n const { settings } = await resolveConfig(storage, cwd);\n const port = settings.find((s) => s.key === \"port\");\n expect(port?.source).toBe(\"db\");\n expect(port?.value).toBe(8080);\n expect(port?.restartRequired).toBe(true);\n });\n\n it(\"restartRequired is false when the DB value equals the booted value\", async () => {\n const storage = new MemoryStorageAdapter();\n // booted with default 3000; persist the same value\n await storage.setConfig(\"port\", 3000);\n const { settings } = await resolveConfig(storage, cwd);\n const port = settings.find((s) => s.key === \"port\");\n expect(port?.restartRequired).toBe(false);\n });\n\n it(\"PUT a restart key 409s when env-pinned (envLocked)\", async () => {\n process.env.PORT = \"4000\";\n resetBootSnapshotForTests();\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/port\", {\n value: 8080,\n });\n expect(status).toBe(409);\n });\n});\n\n// --- Security guardrails ---\n\ndescribe(\"write guardrails (PUT + DELETE)\", () => {\n it(\"PUT rejects `admin` (403) — never web-editable\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/admin\", {\n value: true,\n });\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects `adminAuthToken` (403) — secret\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/adminAuthToken\",\n { value: \"leak\" },\n );\n expect(status).toBe(403);\n });\n\n it(\"PUT rejects an unknown key (404)\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(app, \"PUT\", \"/__enpilink/config/nope\", {\n value: 1,\n });\n expect(status).toBe(404);\n });\n\n it(\"PUT rejects an env-locked runtime key (409)\", async () => {\n process.env.ENPILINK_CFG_FLAGS_LIVE_LOGS = \"false\";\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(\n app,\n \"PUT\",\n \"/__enpilink/config/flags.liveLogs\",\n { value: true },\n );\n expect(status).toBe(409);\n });\n\n it(\"DELETE rejects `admin`/`adminAuthToken`/unknown the same way\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n expect(\n (await request(app, \"DELETE\", \"/__enpilink/config/admin\")).status,\n ).toBe(403);\n expect(\n (await request(app, \"DELETE\", \"/__enpilink/config/adminAuthToken\"))\n .status,\n ).toBe(403);\n expect(\n (await request(app, \"DELETE\", \"/__enpilink/config/nope\")).status,\n ).toBe(404);\n });\n});\n\n// --- Reset to default ---\n\ndescribe(\"reset to default (DELETE)\", () => {\n it(\"clears a DB override and falls back to default + audits\", async () => {\n const storage = new MemoryStorageAdapter();\n await storage.setConfig(\"retention.events\", 9999, \"tester\");\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"DELETE\",\n \"/__enpilink/config/retention.events\",\n );\n expect(status).toBe(200);\n expect((json as { reset: boolean }).reset).toBe(true);\n // Override gone → resolution falls back to the default.\n const { settings } = await resolveConfig(storage, cwd);\n const s = settings.find((x) => x.key === \"retention.events\");\n expect(s?.source).toBe(\"default\");\n expect(s?.value).toBe(5000);\n expect(s?.modified).toBe(false);\n // Audit recorded the reset (most recent first; router actor = \"dev\").\n const audit = await storage.getConfigAudit();\n expect(audit[0]).toMatchObject({ key: \"retention.events\", actor: \"dev\" });\n });\n\n it(\"DELETE with no storage → 409\", async () => {\n const app = appWith(null);\n const { status } = await request(\n app,\n \"DELETE\",\n \"/__enpilink/config/flags.liveLogs\",\n );\n expect(status).toBe(409);\n });\n});\n\n// --- Presets ---\n\ndescribe(\"presets\", () => {\n it(\"GET /config/presets lists Dev + Prod with values\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status, json } = await request(\n app,\n \"GET\",\n \"/__enpilink/config/presets\",\n );\n expect(status).toBe(200);\n const presets = (json as { presets: Array<{ name: string }> }).presets;\n expect(presets.map((p) => p.name).sort()).toEqual([\"dev\", \"prod\"]);\n });\n\n it(\"POST applies a preset to runtime keys + audits\", async () => {\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { status, json } = await request(\n app,\n \"POST\",\n \"/__enpilink/config/preset/prod\",\n );\n expect(status).toBe(200);\n const body = json as {\n applied: { key: string }[];\n skipped: { key: string }[];\n };\n expect(body.applied.length).toBeGreaterThan(0);\n expect(await storage.getConfig(\"analytics.sampleRate\")).toBe(0.25);\n expect(await storage.getConfig(\"flags.liveLogs\")).toBe(false);\n });\n\n it(\"POST skips env-locked keys, reporting them\", async () => {\n process.env.ENPILINK_CFG_ANALYTICS_SAMPLE_RATE = \"1\";\n const storage = new MemoryStorageAdapter();\n const app = appWith(storage);\n const { json } = await request(\n app,\n \"POST\",\n \"/__enpilink/config/preset/prod\",\n );\n const body = json as {\n applied: { key: string }[];\n skipped: { key: string; reason: string }[];\n };\n expect(body.skipped.some((s) => s.key === \"analytics.sampleRate\")).toBe(\n true,\n );\n // env-pinned key was NOT persisted\n expect(await storage.getConfig(\"analytics.sampleRate\")).toBeUndefined();\n });\n\n it(\"POST unknown preset → 404\", async () => {\n const app = appWith(new MemoryStorageAdapter());\n const { status } = await request(\n app,\n \"POST\",\n \"/__enpilink/config/preset/nope\",\n );\n expect(status).toBe(404);\n });\n});\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { getPreset, PRESET_NAMES, PRESETS, type Preset, } from "./presets.js";
|
|
2
|
+
export { type ConfigSource, loadConfigFile, MASKED, type ResolvedConfig, type ResolvedSetting, resolveConfig, validateConfigWrite, validateRuntimeWrite, } from "./resolve.js";
|
|
2
3
|
export { createConfigRouter } from "./router.js";
|
|
3
|
-
export { allKeyMeta, BOOTSTRAP_KEYS, type BootstrapConfig, type BootstrapKey, bootstrapSchema, type Config, type ConfigKey, configSchema, ENV_VARS, isBootstrapKey, isKnownKey, isRuntimeKey, isSecretKey, type KeyMeta, keyMeta, RUNTIME_KEYS, type RuntimeConfig, type RuntimeKey, runtimeSchema, SECRET_KEYS, } from "./schema.js";
|
|
4
|
+
export { allKeyMeta, BOOTSTRAP_KEYS, type BootstrapConfig, type BootstrapKey, bootstrapSchema, type Config, type ConfigKey, configSchema, defaultForKey, type Editable, ENV_VARS, editableOf, isBootstrapKey, isKnownKey, isRestartKey, isRuntimeKey, isSecretKey, type KeyMeta, keyMeta, RESTART_KEYS, RUNTIME_KEYS, type RuntimeConfig, type RuntimeKey, runtimeSchema, SECRET_KEYS, } from "./schema.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { getPreset, PRESET_NAMES, PRESETS, } from "./presets.js";
|
|
2
|
+
export { loadConfigFile, MASKED, resolveConfig, validateConfigWrite, validateRuntimeWrite, } from "./resolve.js";
|
|
2
3
|
export { createConfigRouter } from "./router.js";
|
|
3
|
-
export { allKeyMeta, BOOTSTRAP_KEYS, bootstrapSchema, configSchema, ENV_VARS, isBootstrapKey, isKnownKey, isRuntimeKey, isSecretKey, keyMeta, RUNTIME_KEYS, runtimeSchema, SECRET_KEYS, } from "./schema.js";
|
|
4
|
+
export { allKeyMeta, BOOTSTRAP_KEYS, bootstrapSchema, configSchema, defaultForKey, ENV_VARS, editableOf, isBootstrapKey, isKnownKey, isRestartKey, isRuntimeKey, isSecretKey, keyMeta, RESTART_KEYS, RUNTIME_KEYS, runtimeSchema, SECRET_KEYS, } from "./schema.js";
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,MAAM,EAGN,aAAa,EACb,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,UAAU,EACV,cAAc,EAGd,eAAe,EAGf,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,OAAO,EACP,YAAY,EAGZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC","sourcesContent":["export {\n type ConfigSource,\n loadConfigFile,\n MASKED,\n type ResolvedConfig,\n type ResolvedSetting,\n resolveConfig,\n validateRuntimeWrite,\n} from \"./resolve.js\";\nexport { createConfigRouter } from \"./router.js\";\nexport {\n allKeyMeta,\n BOOTSTRAP_KEYS,\n type BootstrapConfig,\n type BootstrapKey,\n bootstrapSchema,\n type Config,\n type ConfigKey,\n configSchema,\n ENV_VARS,\n isBootstrapKey,\n isKnownKey,\n isRuntimeKey,\n isSecretKey,\n type KeyMeta,\n keyMeta,\n RUNTIME_KEYS,\n type RuntimeConfig,\n type RuntimeKey,\n runtimeSchema,\n SECRET_KEYS,\n} from \"./schema.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,YAAY,EACZ,OAAO,GAER,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,cAAc,EACd,MAAM,EAGN,aAAa,EACb,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,UAAU,EACV,cAAc,EAGd,eAAe,EAGf,YAAY,EACZ,aAAa,EAEb,QAAQ,EACR,UAAU,EACV,cAAc,EACd,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EAEX,OAAO,EACP,YAAY,EACZ,YAAY,EAGZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC","sourcesContent":["export {\n getPreset,\n PRESET_NAMES,\n PRESETS,\n type Preset,\n} from \"./presets.js\";\nexport {\n type ConfigSource,\n loadConfigFile,\n MASKED,\n type ResolvedConfig,\n type ResolvedSetting,\n resolveConfig,\n validateConfigWrite,\n validateRuntimeWrite,\n} from \"./resolve.js\";\nexport { createConfigRouter } from \"./router.js\";\nexport {\n allKeyMeta,\n BOOTSTRAP_KEYS,\n type BootstrapConfig,\n type BootstrapKey,\n bootstrapSchema,\n type Config,\n type ConfigKey,\n configSchema,\n defaultForKey,\n type Editable,\n ENV_VARS,\n editableOf,\n isBootstrapKey,\n isKnownKey,\n isRestartKey,\n isRuntimeKey,\n isSecretKey,\n type KeyMeta,\n keyMeta,\n RESTART_KEYS,\n RUNTIME_KEYS,\n type RuntimeConfig,\n type RuntimeKey,\n runtimeSchema,\n SECRET_KEYS,\n} from \"./schema.js\";\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RuntimeKey } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Config presets / profiles (automation).
|
|
4
|
+
*
|
|
5
|
+
* A preset is a named bundle of RUNTIME-key → value overrides applied in one
|
|
6
|
+
* action. Presets ONLY touch runtime keys — never secrets, never the `admin`
|
|
7
|
+
* gate, never restart-tier bootstrap keys (`port`/`storage`/`dbPath`). The
|
|
8
|
+
* router applies each value through the same validation + audit path as a
|
|
9
|
+
* manual PUT, and skips any key currently pinned by env/file.
|
|
10
|
+
*/
|
|
11
|
+
export interface Preset {
|
|
12
|
+
/** Stable id (used in the URL: `POST /config/preset/:name`). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Human label for the UI button. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** What the preset is for, in one line. */
|
|
17
|
+
description: string;
|
|
18
|
+
/** The runtime-key → value map this preset sets. */
|
|
19
|
+
values: Partial<Record<RuntimeKey, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Built-in presets.
|
|
23
|
+
*
|
|
24
|
+
* - **Dev** — maximum visibility for local development: analytics on, full
|
|
25
|
+
* sampling (record everything), live logs on, generous retention, fine
|
|
26
|
+
* 1-minute chart buckets.
|
|
27
|
+
* - **Prod** — sensible production defaults: analytics on but sampled down to
|
|
28
|
+
* 25% to cut overhead/storage on busy servers, larger retention caps to keep
|
|
29
|
+
* useful history, live logs off (avoid streaming overhead), coarser 5-minute
|
|
30
|
+
* chart buckets.
|
|
31
|
+
*/
|
|
32
|
+
export declare const PRESETS: Record<string, Preset>;
|
|
33
|
+
/** All preset ids. */
|
|
34
|
+
export declare const PRESET_NAMES: string[];
|
|
35
|
+
/** Look up a preset by name (case-insensitive). */
|
|
36
|
+
export declare function getPreset(name: string): Preset | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in presets.
|
|
3
|
+
*
|
|
4
|
+
* - **Dev** — maximum visibility for local development: analytics on, full
|
|
5
|
+
* sampling (record everything), live logs on, generous retention, fine
|
|
6
|
+
* 1-minute chart buckets.
|
|
7
|
+
* - **Prod** — sensible production defaults: analytics on but sampled down to
|
|
8
|
+
* 25% to cut overhead/storage on busy servers, larger retention caps to keep
|
|
9
|
+
* useful history, live logs off (avoid streaming overhead), coarser 5-minute
|
|
10
|
+
* chart buckets.
|
|
11
|
+
*/
|
|
12
|
+
export const PRESETS = {
|
|
13
|
+
dev: {
|
|
14
|
+
name: "dev",
|
|
15
|
+
label: "Dev",
|
|
16
|
+
description: "Maximum visibility for local development: analytics on, full sampling, live logs on, fine-grained charts.",
|
|
17
|
+
values: {
|
|
18
|
+
"analytics.enabled": true,
|
|
19
|
+
"analytics.sampleRate": 1,
|
|
20
|
+
"retention.events": 5000,
|
|
21
|
+
"retention.logs": 5000,
|
|
22
|
+
"flags.liveLogs": true,
|
|
23
|
+
"display.bucketMs": 60_000,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
prod: {
|
|
27
|
+
name: "prod",
|
|
28
|
+
label: "Prod",
|
|
29
|
+
description: "Production-friendly defaults: analytics on but sampled to 25%, larger retention, live logs off, coarser charts.",
|
|
30
|
+
values: {
|
|
31
|
+
"analytics.enabled": true,
|
|
32
|
+
"analytics.sampleRate": 0.25,
|
|
33
|
+
"retention.events": 20_000,
|
|
34
|
+
"retention.logs": 20_000,
|
|
35
|
+
"flags.liveLogs": false,
|
|
36
|
+
"display.bucketMs": 300_000,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** All preset ids. */
|
|
41
|
+
export const PRESET_NAMES = Object.keys(PRESETS);
|
|
42
|
+
/** Look up a preset by name (case-insensitive). */
|
|
43
|
+
export function getPreset(name) {
|
|
44
|
+
return PRESETS[name.toLowerCase()];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=presets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.js","sourceRoot":"","sources":["../../../src/server/config/presets.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,OAAO,GAA2B;IAC7C,GAAG,EAAE;QACH,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,KAAK;QACZ,WAAW,EACT,2GAA2G;QAC7G,MAAM,EAAE;YACN,mBAAmB,EAAE,IAAI;YACzB,sBAAsB,EAAE,CAAC;YACzB,kBAAkB,EAAE,IAAI;YACxB,gBAAgB,EAAE,IAAI;YACtB,gBAAgB,EAAE,IAAI;YACtB,kBAAkB,EAAE,MAAM;SAC3B;KACF;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;QACb,WAAW,EACT,iHAAiH;QACnH,MAAM,EAAE;YACN,mBAAmB,EAAE,IAAI;YACzB,sBAAsB,EAAE,IAAI;YAC5B,kBAAkB,EAAE,MAAM;YAC1B,gBAAgB,EAAE,MAAM;YACxB,gBAAgB,EAAE,KAAK;YACvB,kBAAkB,EAAE,OAAO;SAC5B;KACF;CACF,CAAC;AAEF,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAEjD,mDAAmD;AACnD,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AACrC,CAAC","sourcesContent":["import type { RuntimeKey } from \"./schema.js\";\n\n/**\n * Config presets / profiles (automation).\n *\n * A preset is a named bundle of RUNTIME-key → value overrides applied in one\n * action. Presets ONLY touch runtime keys — never secrets, never the `admin`\n * gate, never restart-tier bootstrap keys (`port`/`storage`/`dbPath`). The\n * router applies each value through the same validation + audit path as a\n * manual PUT, and skips any key currently pinned by env/file.\n */\n\nexport interface Preset {\n /** Stable id (used in the URL: `POST /config/preset/:name`). */\n name: string;\n /** Human label for the UI button. */\n label: string;\n /** What the preset is for, in one line. */\n description: string;\n /** The runtime-key → value map this preset sets. */\n values: Partial<Record<RuntimeKey, unknown>>;\n}\n\n/**\n * Built-in presets.\n *\n * - **Dev** — maximum visibility for local development: analytics on, full\n * sampling (record everything), live logs on, generous retention, fine\n * 1-minute chart buckets.\n * - **Prod** — sensible production defaults: analytics on but sampled down to\n * 25% to cut overhead/storage on busy servers, larger retention caps to keep\n * useful history, live logs off (avoid streaming overhead), coarser 5-minute\n * chart buckets.\n */\nexport const PRESETS: Record<string, Preset> = {\n dev: {\n name: \"dev\",\n label: \"Dev\",\n description:\n \"Maximum visibility for local development: analytics on, full sampling, live logs on, fine-grained charts.\",\n values: {\n \"analytics.enabled\": true,\n \"analytics.sampleRate\": 1,\n \"retention.events\": 5000,\n \"retention.logs\": 5000,\n \"flags.liveLogs\": true,\n \"display.bucketMs\": 60_000,\n },\n },\n prod: {\n name: \"prod\",\n label: \"Prod\",\n description:\n \"Production-friendly defaults: analytics on but sampled to 25%, larger retention, live logs off, coarser charts.\",\n values: {\n \"analytics.enabled\": true,\n \"analytics.sampleRate\": 0.25,\n \"retention.events\": 20_000,\n \"retention.logs\": 20_000,\n \"flags.liveLogs\": false,\n \"display.bucketMs\": 300_000,\n },\n },\n};\n\n/** All preset ids. */\nexport const PRESET_NAMES = Object.keys(PRESETS);\n\n/** Look up a preset by name (case-insensitive). */\nexport function getPreset(name: string): Preset | undefined {\n return PRESETS[name.toLowerCase()];\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StorageAdapter } from "../storage/types.js";
|
|
2
|
-
import { type Config, type ConfigKey } from "./schema.js";
|
|
2
|
+
import { type Config, type ConfigKey, type Editable } from "./schema.js";
|
|
3
3
|
/**
|
|
4
4
|
* Config resolution (M4). Merges sources with precedence
|
|
5
5
|
* **env > file (`enpilink.config.{json,ts}`) > db (runtime only) > default**
|
|
@@ -26,12 +26,37 @@ export interface ResolvedSetting {
|
|
|
26
26
|
/** Whether this key is a secret (never returned in plaintext). */
|
|
27
27
|
secret: boolean;
|
|
28
28
|
/**
|
|
29
|
-
* Whether this key is
|
|
30
|
-
*
|
|
29
|
+
* Whether this key is pinned by env/file (the DB value is shadowed). The UI
|
|
30
|
+
* renders these read-only with a "set via ENV_VAR" hint. NOTE: a `restart`
|
|
31
|
+
* key that is NOT env/file-pinned is editable here even though
|
|
32
|
+
* `editable === "restart"`.
|
|
31
33
|
*/
|
|
32
34
|
envLocked: boolean;
|
|
33
35
|
/** The env var that drives / can pin this key. */
|
|
34
36
|
env: string;
|
|
37
|
+
/** Human-friendly label. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Plain-language one-liner describing the setting. */
|
|
40
|
+
description: string;
|
|
41
|
+
/** Functional group for UI sectioning (e.g. "Analytics", "Server"). */
|
|
42
|
+
group: string;
|
|
43
|
+
/** Optional unit hint (e.g. "ms", "events", "0–1 ratio"). */
|
|
44
|
+
unit?: string;
|
|
45
|
+
/** The schema default value. */
|
|
46
|
+
default: unknown;
|
|
47
|
+
/** Editability classification: `runtime` | `restart` | `readonly`. */
|
|
48
|
+
editable: Editable;
|
|
49
|
+
/**
|
|
50
|
+
* True when the effective value comes from a DB override that differs from
|
|
51
|
+
* the schema default (i.e. the operator has changed it).
|
|
52
|
+
*/
|
|
53
|
+
modified: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Restart-tier only: true when a persisted DB value differs from the value
|
|
56
|
+
* this process actually booted with — a pending change awaiting restart.
|
|
57
|
+
* Always `false` for non-restart keys.
|
|
58
|
+
*/
|
|
59
|
+
restartRequired: boolean;
|
|
35
60
|
}
|
|
36
61
|
/** The full resolved config plus per-key reporting. */
|
|
37
62
|
export interface ResolvedConfig {
|
|
@@ -50,6 +75,8 @@ export declare function loadConfigFile(cwd?: string): {
|
|
|
50
75
|
source: "file" | null;
|
|
51
76
|
values: Partial<Record<ConfigKey, unknown>>;
|
|
52
77
|
};
|
|
78
|
+
/** TEST-ONLY: reset the memoized boot snapshot so each test recomputes it. */
|
|
79
|
+
export declare function resetBootSnapshotForTests(): void;
|
|
53
80
|
/**
|
|
54
81
|
* Resolve all config. Reads the DB (runtime keys only) when a storage adapter
|
|
55
82
|
* is provided; secrets are never read from the DB. Falls back to defaults when
|
|
@@ -71,3 +98,15 @@ export declare function validateRuntimeWrite(key: string, rawValue: unknown): {
|
|
|
71
98
|
ok: false;
|
|
72
99
|
error: string;
|
|
73
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Validate + coerce a single WRITABLE value (runtime OR restart tier) before
|
|
103
|
+
* persisting. Rejects secret keys and any key that is not writable from the
|
|
104
|
+
* admin UI (`readonly`/unknown). Used by the router's PUT + preset/reset paths.
|
|
105
|
+
*/
|
|
106
|
+
export declare function validateConfigWrite(key: string, rawValue: unknown): {
|
|
107
|
+
ok: true;
|
|
108
|
+
value: unknown;
|
|
109
|
+
} | {
|
|
110
|
+
ok: false;
|
|
111
|
+
error: string;
|
|
112
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { coerceBool, coerceNumber, configSchema, ENV_VARS, isRuntimeKey, isSecretKey, keyMeta, RUNTIME_KEYS, schemaForKey, } from "./schema.js";
|
|
3
|
+
import { coerceBool, coerceNumber, configSchema, defaultForKey, ENV_VARS, isRestartKey, isRuntimeKey, isSecretKey, keyMeta, RESTART_KEYS, RUNTIME_KEYS, schemaForKey, } from "./schema.js";
|
|
4
4
|
/** A masked placeholder returned in place of any secret value. */
|
|
5
5
|
export const MASKED = "••••••••";
|
|
6
6
|
const FILE_NAMES = ["enpilink.config.json", "enpilink.config.ts"];
|
|
@@ -39,6 +39,42 @@ function envValue(key) {
|
|
|
39
39
|
}
|
|
40
40
|
/** Default-typed sample used to learn each key's expected runtime type. */
|
|
41
41
|
const TYPE_SAMPLE = configSchema.parse({});
|
|
42
|
+
/**
|
|
43
|
+
* The value each restart-tier key BOOTED with, computed once at module load
|
|
44
|
+
* from env > file > default (NEVER the DB — the running process never reads
|
|
45
|
+
* restart keys from the DB). A later persisted DB value that differs from this
|
|
46
|
+
* snapshot is a pending change awaiting restart (`restartRequired`).
|
|
47
|
+
*
|
|
48
|
+
* Memoized: captured lazily on first read so tests that set env/cwd before the
|
|
49
|
+
* first `resolveConfig` call observe the right boot values, and it stays stable
|
|
50
|
+
* for the lifetime of the process thereafter.
|
|
51
|
+
*/
|
|
52
|
+
let bootSnapshot = null;
|
|
53
|
+
function getBootSnapshot(cwd) {
|
|
54
|
+
if (bootSnapshot) {
|
|
55
|
+
return bootSnapshot;
|
|
56
|
+
}
|
|
57
|
+
const file = loadConfigFile(cwd);
|
|
58
|
+
const snap = {};
|
|
59
|
+
for (const key of RESTART_KEYS) {
|
|
60
|
+
const env = envValue(key);
|
|
61
|
+
if (env !== undefined) {
|
|
62
|
+
snap[key] = coerceForKey(key, env);
|
|
63
|
+
}
|
|
64
|
+
else if (key in file.values) {
|
|
65
|
+
snap[key] = file.values[key];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
snap[key] = defaultForKey(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
bootSnapshot = snap;
|
|
72
|
+
return snap;
|
|
73
|
+
}
|
|
74
|
+
/** TEST-ONLY: reset the memoized boot snapshot so each test recomputes it. */
|
|
75
|
+
export function resetBootSnapshotForTests() {
|
|
76
|
+
bootSnapshot = null;
|
|
77
|
+
}
|
|
42
78
|
/** Coerce a raw value (e.g. an env string) to the key's expected type. */
|
|
43
79
|
function coerceForKey(key, raw) {
|
|
44
80
|
const expected = typeof TYPE_SAMPLE[key];
|
|
@@ -60,12 +96,12 @@ function coerceForKey(key, raw) {
|
|
|
60
96
|
*/
|
|
61
97
|
export async function resolveConfig(storage, cwd = process.cwd()) {
|
|
62
98
|
const file = loadConfigFile(cwd);
|
|
63
|
-
// DB values for runtime keys
|
|
99
|
+
// DB values for runtime + restart-tier keys (secrets are never persisted/read).
|
|
64
100
|
let db = {};
|
|
65
101
|
if (storage) {
|
|
66
102
|
try {
|
|
67
103
|
const all = await storage.allConfig();
|
|
68
|
-
for (const key of RUNTIME_KEYS) {
|
|
104
|
+
for (const key of [...RUNTIME_KEYS, ...RESTART_KEYS]) {
|
|
69
105
|
if (key in all && !isSecretKey(key)) {
|
|
70
106
|
db[key] = all[key];
|
|
71
107
|
}
|
|
@@ -90,8 +126,10 @@ export async function resolveConfig(storage, cwd = process.cwd()) {
|
|
|
90
126
|
sources.set(key, "file");
|
|
91
127
|
continue;
|
|
92
128
|
}
|
|
93
|
-
//
|
|
94
|
-
if (isRuntimeKey(key)
|
|
129
|
+
// Runtime + restart-tier keys may be sourced from the DB. Secrets never are.
|
|
130
|
+
if ((isRuntimeKey(key) || isRestartKey(key)) &&
|
|
131
|
+
!isSecretKey(key) &&
|
|
132
|
+
key in db) {
|
|
95
133
|
rawValues[key] = db[key];
|
|
96
134
|
sources.set(key, "db");
|
|
97
135
|
continue;
|
|
@@ -118,18 +156,27 @@ export async function resolveConfig(storage, cwd = process.cwd()) {
|
|
|
118
156
|
}
|
|
119
157
|
values = configSchema.parse(safe);
|
|
120
158
|
}
|
|
159
|
+
const boot = getBootSnapshot(cwd);
|
|
121
160
|
const settings = keys.map((key) => {
|
|
122
161
|
const meta = keyMeta(key);
|
|
123
162
|
const source = sources.get(key) ?? "default";
|
|
124
163
|
const secret = meta.secret;
|
|
125
|
-
// Env-locked
|
|
126
|
-
//
|
|
127
|
-
|
|
164
|
+
// Env-locked (read-only in the UI) when:
|
|
165
|
+
// - the key is `readonly` (admin gate + the secret), OR
|
|
166
|
+
// - the key is pinned via env or file (a DB write would be shadowed).
|
|
167
|
+
// A `restart` key NOT pinned by env/file is editable (just needs a restart).
|
|
168
|
+
const envLocked = meta.editable === "readonly" || source === "env" || source === "file";
|
|
128
169
|
const value = secret
|
|
129
170
|
? values[key] !== undefined && values[key] !== ""
|
|
130
171
|
? MASKED
|
|
131
172
|
: null
|
|
132
173
|
: values[key];
|
|
174
|
+
// `modified` — the effective value is a DB override that differs from the
|
|
175
|
+
// schema default.
|
|
176
|
+
const modified = !secret && source === "db" && !valuesEqual(values[key], meta.default);
|
|
177
|
+
// `restartRequired` — restart-tier key whose persisted DB value differs
|
|
178
|
+
// from the value this process booted with.
|
|
179
|
+
const restartRequired = isRestartKey(key) && key in db && !valuesEqual(db[key], boot[key]);
|
|
133
180
|
return {
|
|
134
181
|
key,
|
|
135
182
|
tier: meta.tier,
|
|
@@ -138,10 +185,29 @@ export async function resolveConfig(storage, cwd = process.cwd()) {
|
|
|
138
185
|
secret,
|
|
139
186
|
envLocked,
|
|
140
187
|
env: meta.env,
|
|
188
|
+
label: meta.label,
|
|
189
|
+
description: meta.description,
|
|
190
|
+
group: meta.group,
|
|
191
|
+
unit: meta.unit,
|
|
192
|
+
default: meta.default,
|
|
193
|
+
editable: meta.editable,
|
|
194
|
+
modified,
|
|
195
|
+
restartRequired,
|
|
141
196
|
};
|
|
142
197
|
});
|
|
143
198
|
return { values, settings };
|
|
144
199
|
}
|
|
200
|
+
/** Structural value-equality for JSON-ish config values (primitives only here). */
|
|
201
|
+
function valuesEqual(a, b) {
|
|
202
|
+
if (a === b) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
// Treat numerically-equal values across number/string coercion as equal.
|
|
206
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
207
|
+
return a === b;
|
|
208
|
+
}
|
|
209
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
210
|
+
}
|
|
145
211
|
/**
|
|
146
212
|
* Validate + coerce a single RUNTIME, non-secret value before persisting.
|
|
147
213
|
* Returns the coerced value or an error string. Rejects unknown/bootstrap/
|
|
@@ -151,6 +217,20 @@ export function validateRuntimeWrite(key, rawValue) {
|
|
|
151
217
|
if (!isRuntimeKey(key)) {
|
|
152
218
|
return { ok: false, error: `"${key}" is not a runtime key` };
|
|
153
219
|
}
|
|
220
|
+
return validateAgainstSchema(key, rawValue);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Validate + coerce a single WRITABLE value (runtime OR restart tier) before
|
|
224
|
+
* persisting. Rejects secret keys and any key that is not writable from the
|
|
225
|
+
* admin UI (`readonly`/unknown). Used by the router's PUT + preset/reset paths.
|
|
226
|
+
*/
|
|
227
|
+
export function validateConfigWrite(key, rawValue) {
|
|
228
|
+
if (!isRuntimeKey(key) && !isRestartKey(key)) {
|
|
229
|
+
return { ok: false, error: `"${key}" is not editable` };
|
|
230
|
+
}
|
|
231
|
+
return validateAgainstSchema(key, rawValue);
|
|
232
|
+
}
|
|
233
|
+
function validateAgainstSchema(key, rawValue) {
|
|
154
234
|
if (isSecretKey(key)) {
|
|
155
235
|
return { ok: false, error: `"${key}" is a secret and cannot be set here` };
|
|
156
236
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/server/config/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { StorageAdapter } from \"../storage/types.js\";\nimport {\n type Config,\n type ConfigKey,\n coerceBool,\n coerceNumber,\n configSchema,\n ENV_VARS,\n isRuntimeKey,\n isSecretKey,\n keyMeta,\n RUNTIME_KEYS,\n schemaForKey,\n} from \"./schema.js\";\n\n/**\n * Config resolution (M4). Merges sources with precedence\n * **env > file (`enpilink.config.{json,ts}`) > db (runtime only) > default**\n * and reports, per key, which source supplied the value and whether the key is\n * secret / env-locked (read-only).\n *\n * Secrets (e.g. `adminAuthToken`) are NEVER read from the DB and NEVER returned\n * in plaintext — {@link resolveConfig} masks them to a placeholder and flags\n * `secret: true`. Bootstrap keys and any key pinned by env/file are\n * `envLocked: true` (the admin UI must render them read-only).\n */\n\nexport type ConfigSource = \"env\" | \"file\" | \"db\" | \"default\";\n\n/** A masked placeholder returned in place of any secret value. */\nexport const MASKED = \"••••••••\";\n\n/** A single resolved setting as exposed by the config API. */\nexport interface ResolvedSetting {\n key: ConfigKey;\n /** Tier: `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** The resolved value, or {@link MASKED} when secret. */\n value: unknown;\n /** Which source supplied the value. */\n source: ConfigSource;\n /** Whether this key is a secret (never returned in plaintext). */\n secret: boolean;\n /**\n * Whether this key is read-only in the admin UI. True for all bootstrap keys,\n * and for any runtime key pinned via env or file (the DB value is shadowed).\n */\n envLocked: boolean;\n /** The env var that drives / can pin this key. */\n env: string;\n}\n\n/** The full resolved config plus per-key reporting. */\nexport interface ResolvedConfig {\n /** Effective typed values (secrets present in-process, masked only at the API). */\n values: Config;\n /** Per-key source + secret/env-lock reporting. */\n settings: ResolvedSetting[];\n}\n\nconst FILE_NAMES = [\"enpilink.config.json\", \"enpilink.config.ts\"];\n\n/**\n * Load the optional config file (JSON only — a `.ts` file is acknowledged but\n * not executed here to avoid a runtime transpile dependency; M6 can add TS\n * loading). Returns a flat partial keyed by {@link ConfigKey}. Never throws —\n * a malformed/absent file yields `{}`.\n */\nexport function loadConfigFile(cwd: string = process.cwd()): {\n source: \"file\" | null;\n values: Partial<Record<ConfigKey, unknown>>;\n} {\n for (const name of FILE_NAMES) {\n const full = path.join(cwd, name);\n if (!fs.existsSync(full)) {\n continue;\n }\n if (name.endsWith(\".json\")) {\n try {\n const raw = fs.readFileSync(full, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return { source: \"file\", values: parsed as Record<string, unknown> };\n }\n } catch {\n // Malformed file → ignore (defaults/db/env still apply).\n }\n }\n // A `.ts` config file is recognized but not executed in M4.\n }\n return { source: null, values: {} };\n}\n\n/** Read the raw env value for a key (the mapped env var), or undefined. */\nfunction envValue(key: ConfigKey): string | undefined {\n const raw = process.env[ENV_VARS[key]];\n return raw === undefined || raw === \"\" ? undefined : raw;\n}\n\n/** Default-typed sample used to learn each key's expected runtime type. */\nconst TYPE_SAMPLE = configSchema.parse({}) as Record<string, unknown>;\n\n/** Coerce a raw value (e.g. an env string) to the key's expected type. */\nfunction coerceForKey(key: ConfigKey, raw: unknown): unknown {\n const expected = typeof TYPE_SAMPLE[key];\n if (expected === \"boolean\") {\n return coerceBool(raw) ?? raw;\n }\n if (expected === \"number\") {\n return coerceNumber(raw) ?? raw;\n }\n return raw;\n}\n\n/**\n * Resolve all config. Reads the DB (runtime keys only) when a storage adapter\n * is provided; secrets are never read from the DB. Falls back to defaults when\n * there is no storage. Never throws on a storage error — it degrades to\n * file/env/default.\n *\n * @param storage active storage adapter, or `null` when analytics/admin is off.\n */\nexport async function resolveConfig(\n storage: StorageAdapter | null,\n cwd: string = process.cwd(),\n): Promise<ResolvedConfig> {\n const file = loadConfigFile(cwd);\n\n // DB values for runtime keys only (secrets are never persisted/read).\n let db: Record<string, unknown> = {};\n if (storage) {\n try {\n const all = await storage.allConfig();\n for (const key of RUNTIME_KEYS) {\n if (key in all && !isSecretKey(key)) {\n db[key] = all[key];\n }\n }\n } catch {\n db = {};\n }\n }\n\n const rawValues: Record<string, unknown> = {};\n const sources = new Map<ConfigKey, ConfigSource>();\n\n const keys = Object.keys(configSchema.shape) as ConfigKey[];\n for (const key of keys) {\n const env = envValue(key);\n if (env !== undefined) {\n rawValues[key] = coerceForKey(key, env);\n sources.set(key, \"env\");\n continue;\n }\n if (key in file.values) {\n rawValues[key] = file.values[key];\n sources.set(key, \"file\");\n continue;\n }\n // Secrets are never read from the DB.\n if (isRuntimeKey(key) && !isSecretKey(key) && key in db) {\n rawValues[key] = db[key];\n sources.set(key, \"db\");\n continue;\n }\n sources.set(key, \"default\");\n }\n\n // Validate + fill defaults. If a supplied value is invalid, zod throws; we\n // fall back per-key to the default so a bad DB/file value can't crash reads.\n let values: Config;\n try {\n values = configSchema.parse(rawValues);\n } catch {\n // Re-resolve key-by-key, dropping any invalid override.\n const safe: Record<string, unknown> = {};\n for (const key of keys) {\n const single = schemaForKey(key).safeParse(rawValues[key]);\n if (single.success && key in rawValues) {\n safe[key] = rawValues[key];\n } else if (sources.get(key) !== \"default\") {\n sources.set(key, \"default\");\n }\n }\n values = configSchema.parse(safe);\n }\n\n const settings: ResolvedSetting[] = keys.map((key) => {\n const meta = keyMeta(key);\n const source = sources.get(key) ?? \"default\";\n const secret = meta.secret;\n // Env-locked: bootstrap keys are always read-only; runtime keys are locked\n // when pinned by env or file (the DB write would be shadowed).\n const envLocked =\n meta.tier === \"bootstrap\" || source === \"env\" || source === \"file\";\n const value = secret\n ? values[key] !== undefined && values[key] !== \"\"\n ? MASKED\n : null\n : (values[key] as unknown);\n return {\n key,\n tier: meta.tier,\n value,\n source,\n secret,\n envLocked,\n env: meta.env,\n };\n });\n\n return { values, settings };\n}\n\n/**\n * Validate + coerce a single RUNTIME, non-secret value before persisting.\n * Returns the coerced value or an error string. Rejects unknown/bootstrap/\n * secret keys (those are handled by the caller with a clear 4xx).\n */\nexport function validateRuntimeWrite(\n key: string,\n rawValue: unknown,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (!isRuntimeKey(key)) {\n return { ok: false, error: `\"${key}\" is not a runtime key` };\n }\n if (isSecretKey(key)) {\n return { ok: false, error: `\"${key}\" is a secret and cannot be set here` };\n }\n const coerced = coerceForKey(key as ConfigKey, rawValue);\n const parsed = schemaForKey(key as ConfigKey).safeParse(coerced);\n if (!parsed.success) {\n return {\n ok: false,\n error: `invalid value for \"${key}\": ${parsed.error.issues[0]?.message ?? \"validation failed\"}`,\n };\n }\n return { ok: true, value: parsed.data };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/server/config/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { StorageAdapter } from \"../storage/types.js\";\nimport {\n type Config,\n type ConfigKey,\n coerceBool,\n coerceNumber,\n configSchema,\n defaultForKey,\n type Editable,\n ENV_VARS,\n isRestartKey,\n isRuntimeKey,\n isSecretKey,\n keyMeta,\n RESTART_KEYS,\n RUNTIME_KEYS,\n schemaForKey,\n} from \"./schema.js\";\n\n/**\n * Config resolution (M4). Merges sources with precedence\n * **env > file (`enpilink.config.{json,ts}`) > db (runtime only) > default**\n * and reports, per key, which source supplied the value and whether the key is\n * secret / env-locked (read-only).\n *\n * Secrets (e.g. `adminAuthToken`) are NEVER read from the DB and NEVER returned\n * in plaintext — {@link resolveConfig} masks them to a placeholder and flags\n * `secret: true`. Bootstrap keys and any key pinned by env/file are\n * `envLocked: true` (the admin UI must render them read-only).\n */\n\nexport type ConfigSource = \"env\" | \"file\" | \"db\" | \"default\";\n\n/** A masked placeholder returned in place of any secret value. */\nexport const MASKED = \"••••••••\";\n\n/** A single resolved setting as exposed by the config API. */\nexport interface ResolvedSetting {\n key: ConfigKey;\n /** Tier: `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** The resolved value, or {@link MASKED} when secret. */\n value: unknown;\n /** Which source supplied the value. */\n source: ConfigSource;\n /** Whether this key is a secret (never returned in plaintext). */\n secret: boolean;\n /**\n * Whether this key is pinned by env/file (the DB value is shadowed). The UI\n * renders these read-only with a \"set via ENV_VAR\" hint. NOTE: a `restart`\n * key that is NOT env/file-pinned is editable here even though\n * `editable === \"restart\"`.\n */\n envLocked: boolean;\n /** The env var that drives / can pin this key. */\n env: string;\n /** Human-friendly label. */\n label: string;\n /** Plain-language one-liner describing the setting. */\n description: string;\n /** Functional group for UI sectioning (e.g. \"Analytics\", \"Server\"). */\n group: string;\n /** Optional unit hint (e.g. \"ms\", \"events\", \"0–1 ratio\"). */\n unit?: string;\n /** The schema default value. */\n default: unknown;\n /** Editability classification: `runtime` | `restart` | `readonly`. */\n editable: Editable;\n /**\n * True when the effective value comes from a DB override that differs from\n * the schema default (i.e. the operator has changed it).\n */\n modified: boolean;\n /**\n * Restart-tier only: true when a persisted DB value differs from the value\n * this process actually booted with — a pending change awaiting restart.\n * Always `false` for non-restart keys.\n */\n restartRequired: boolean;\n}\n\n/** The full resolved config plus per-key reporting. */\nexport interface ResolvedConfig {\n /** Effective typed values (secrets present in-process, masked only at the API). */\n values: Config;\n /** Per-key source + secret/env-lock reporting. */\n settings: ResolvedSetting[];\n}\n\nconst FILE_NAMES = [\"enpilink.config.json\", \"enpilink.config.ts\"];\n\n/**\n * Load the optional config file (JSON only — a `.ts` file is acknowledged but\n * not executed here to avoid a runtime transpile dependency; M6 can add TS\n * loading). Returns a flat partial keyed by {@link ConfigKey}. Never throws —\n * a malformed/absent file yields `{}`.\n */\nexport function loadConfigFile(cwd: string = process.cwd()): {\n source: \"file\" | null;\n values: Partial<Record<ConfigKey, unknown>>;\n} {\n for (const name of FILE_NAMES) {\n const full = path.join(cwd, name);\n if (!fs.existsSync(full)) {\n continue;\n }\n if (name.endsWith(\".json\")) {\n try {\n const raw = fs.readFileSync(full, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return { source: \"file\", values: parsed as Record<string, unknown> };\n }\n } catch {\n // Malformed file → ignore (defaults/db/env still apply).\n }\n }\n // A `.ts` config file is recognized but not executed in M4.\n }\n return { source: null, values: {} };\n}\n\n/** Read the raw env value for a key (the mapped env var), or undefined. */\nfunction envValue(key: ConfigKey): string | undefined {\n const raw = process.env[ENV_VARS[key]];\n return raw === undefined || raw === \"\" ? undefined : raw;\n}\n\n/** Default-typed sample used to learn each key's expected runtime type. */\nconst TYPE_SAMPLE = configSchema.parse({}) as Record<string, unknown>;\n\n/**\n * The value each restart-tier key BOOTED with, computed once at module load\n * from env > file > default (NEVER the DB — the running process never reads\n * restart keys from the DB). A later persisted DB value that differs from this\n * snapshot is a pending change awaiting restart (`restartRequired`).\n *\n * Memoized: captured lazily on first read so tests that set env/cwd before the\n * first `resolveConfig` call observe the right boot values, and it stays stable\n * for the lifetime of the process thereafter.\n */\nlet bootSnapshot: Record<string, unknown> | null = null;\nfunction getBootSnapshot(cwd: string): Record<string, unknown> {\n if (bootSnapshot) {\n return bootSnapshot;\n }\n const file = loadConfigFile(cwd);\n const snap: Record<string, unknown> = {};\n for (const key of RESTART_KEYS) {\n const env = envValue(key);\n if (env !== undefined) {\n snap[key] = coerceForKey(key, env);\n } else if (key in file.values) {\n snap[key] = file.values[key];\n } else {\n snap[key] = defaultForKey(key);\n }\n }\n bootSnapshot = snap;\n return snap;\n}\n\n/** TEST-ONLY: reset the memoized boot snapshot so each test recomputes it. */\nexport function resetBootSnapshotForTests(): void {\n bootSnapshot = null;\n}\n\n/** Coerce a raw value (e.g. an env string) to the key's expected type. */\nfunction coerceForKey(key: ConfigKey, raw: unknown): unknown {\n const expected = typeof TYPE_SAMPLE[key];\n if (expected === \"boolean\") {\n return coerceBool(raw) ?? raw;\n }\n if (expected === \"number\") {\n return coerceNumber(raw) ?? raw;\n }\n return raw;\n}\n\n/**\n * Resolve all config. Reads the DB (runtime keys only) when a storage adapter\n * is provided; secrets are never read from the DB. Falls back to defaults when\n * there is no storage. Never throws on a storage error — it degrades to\n * file/env/default.\n *\n * @param storage active storage adapter, or `null` when analytics/admin is off.\n */\nexport async function resolveConfig(\n storage: StorageAdapter | null,\n cwd: string = process.cwd(),\n): Promise<ResolvedConfig> {\n const file = loadConfigFile(cwd);\n\n // DB values for runtime + restart-tier keys (secrets are never persisted/read).\n let db: Record<string, unknown> = {};\n if (storage) {\n try {\n const all = await storage.allConfig();\n for (const key of [...RUNTIME_KEYS, ...RESTART_KEYS]) {\n if (key in all && !isSecretKey(key)) {\n db[key] = all[key];\n }\n }\n } catch {\n db = {};\n }\n }\n\n const rawValues: Record<string, unknown> = {};\n const sources = new Map<ConfigKey, ConfigSource>();\n\n const keys = Object.keys(configSchema.shape) as ConfigKey[];\n for (const key of keys) {\n const env = envValue(key);\n if (env !== undefined) {\n rawValues[key] = coerceForKey(key, env);\n sources.set(key, \"env\");\n continue;\n }\n if (key in file.values) {\n rawValues[key] = file.values[key];\n sources.set(key, \"file\");\n continue;\n }\n // Runtime + restart-tier keys may be sourced from the DB. Secrets never are.\n if (\n (isRuntimeKey(key) || isRestartKey(key)) &&\n !isSecretKey(key) &&\n key in db\n ) {\n rawValues[key] = db[key];\n sources.set(key, \"db\");\n continue;\n }\n sources.set(key, \"default\");\n }\n\n // Validate + fill defaults. If a supplied value is invalid, zod throws; we\n // fall back per-key to the default so a bad DB/file value can't crash reads.\n let values: Config;\n try {\n values = configSchema.parse(rawValues);\n } catch {\n // Re-resolve key-by-key, dropping any invalid override.\n const safe: Record<string, unknown> = {};\n for (const key of keys) {\n const single = schemaForKey(key).safeParse(rawValues[key]);\n if (single.success && key in rawValues) {\n safe[key] = rawValues[key];\n } else if (sources.get(key) !== \"default\") {\n sources.set(key, \"default\");\n }\n }\n values = configSchema.parse(safe);\n }\n\n const boot = getBootSnapshot(cwd);\n\n const settings: ResolvedSetting[] = keys.map((key) => {\n const meta = keyMeta(key);\n const source = sources.get(key) ?? \"default\";\n const secret = meta.secret;\n // Env-locked (read-only in the UI) when:\n // - the key is `readonly` (admin gate + the secret), OR\n // - the key is pinned via env or file (a DB write would be shadowed).\n // A `restart` key NOT pinned by env/file is editable (just needs a restart).\n const envLocked =\n meta.editable === \"readonly\" || source === \"env\" || source === \"file\";\n const value = secret\n ? values[key] !== undefined && values[key] !== \"\"\n ? MASKED\n : null\n : (values[key] as unknown);\n // `modified` — the effective value is a DB override that differs from the\n // schema default.\n const modified =\n !secret && source === \"db\" && !valuesEqual(values[key], meta.default);\n // `restartRequired` — restart-tier key whose persisted DB value differs\n // from the value this process booted with.\n const restartRequired =\n isRestartKey(key) && key in db && !valuesEqual(db[key], boot[key]);\n return {\n key,\n tier: meta.tier,\n value,\n source,\n secret,\n envLocked,\n env: meta.env,\n label: meta.label,\n description: meta.description,\n group: meta.group,\n unit: meta.unit,\n default: meta.default,\n editable: meta.editable,\n modified,\n restartRequired,\n };\n });\n\n return { values, settings };\n}\n\n/** Structural value-equality for JSON-ish config values (primitives only here). */\nfunction valuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true;\n }\n // Treat numerically-equal values across number/string coercion as equal.\n if (typeof a === \"number\" && typeof b === \"number\") {\n return a === b;\n }\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * Validate + coerce a single RUNTIME, non-secret value before persisting.\n * Returns the coerced value or an error string. Rejects unknown/bootstrap/\n * secret keys (those are handled by the caller with a clear 4xx).\n */\nexport function validateRuntimeWrite(\n key: string,\n rawValue: unknown,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (!isRuntimeKey(key)) {\n return { ok: false, error: `\"${key}\" is not a runtime key` };\n }\n return validateAgainstSchema(key, rawValue);\n}\n\n/**\n * Validate + coerce a single WRITABLE value (runtime OR restart tier) before\n * persisting. Rejects secret keys and any key that is not writable from the\n * admin UI (`readonly`/unknown). Used by the router's PUT + preset/reset paths.\n */\nexport function validateConfigWrite(\n key: string,\n rawValue: unknown,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (!isRuntimeKey(key) && !isRestartKey(key)) {\n return { ok: false, error: `\"${key}\" is not editable` };\n }\n return validateAgainstSchema(key, rawValue);\n}\n\nfunction validateAgainstSchema(\n key: string,\n rawValue: unknown,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (isSecretKey(key)) {\n return { ok: false, error: `\"${key}\" is a secret and cannot be set here` };\n }\n const coerced = coerceForKey(key as ConfigKey, rawValue);\n const parsed = schemaForKey(key as ConfigKey).safeParse(coerced);\n if (!parsed.success) {\n return {\n ok: false,\n error: `invalid value for \"${key}\": ${parsed.error.issues[0]?.message ?? \"validation failed\"}`,\n };\n }\n return { ok: true, value: parsed.data };\n}\n"]}
|