enpilink 1.0.2 → 1.0.3

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.
Files changed (38) hide show
  1. package/dist/server/config/config.test.js +201 -8
  2. package/dist/server/config/config.test.js.map +1 -1
  3. package/dist/server/config/index.d.ts +3 -2
  4. package/dist/server/config/index.js +3 -2
  5. package/dist/server/config/index.js.map +1 -1
  6. package/dist/server/config/presets.d.ts +36 -0
  7. package/dist/server/config/presets.js +46 -0
  8. package/dist/server/config/presets.js.map +1 -0
  9. package/dist/server/config/resolve.d.ts +42 -3
  10. package/dist/server/config/resolve.js +88 -8
  11. package/dist/server/config/resolve.js.map +1 -1
  12. package/dist/server/config/router.d.ts +22 -14
  13. package/dist/server/config/router.js +153 -51
  14. package/dist/server/config/router.js.map +1 -1
  15. package/dist/server/config/schema.d.ts +39 -1
  16. package/dist/server/config/schema.js +121 -0
  17. package/dist/server/config/schema.js.map +1 -1
  18. package/dist/server/index.d.ts +1 -1
  19. package/dist/server/index.js +1 -1
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/storage/memory.d.ts +1 -0
  22. package/dist/server/storage/memory.js +14 -0
  23. package/dist/server/storage/memory.js.map +1 -1
  24. package/dist/server/storage/memory.test.js +17 -0
  25. package/dist/server/storage/memory.test.js.map +1 -1
  26. package/dist/server/storage/postgres.d.ts +1 -0
  27. package/dist/server/storage/postgres.js +12 -0
  28. package/dist/server/storage/postgres.js.map +1 -1
  29. package/dist/server/storage/postgres.test.js +17 -0
  30. package/dist/server/storage/postgres.test.js.map +1 -1
  31. package/dist/server/storage/sqlite.d.ts +1 -0
  32. package/dist/server/storage/sqlite.js +21 -0
  33. package/dist/server/storage/sqlite.js.map +1 -1
  34. package/dist/server/storage/sqlite.test.js +17 -0
  35. package/dist/server/storage/sqlite.test.js.map +1 -1
  36. package/dist/server/storage/types.d.ts +6 -0
  37. package/dist/server/storage/types.js.map +1 -1
  38. package/package.json +2 -2
@@ -4,21 +4,28 @@ import path from "node:path";
4
4
  import express from "express";
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
6
  import { MemoryStorageAdapter } from "../storage/memory.js";
7
- import { MASKED, resolveConfig, validateRuntimeWrite } from "./resolve.js";
7
+ import { MASKED, resetBootSnapshotForTests, resolveConfig, validateConfigWrite, validateRuntimeWrite, } from "./resolve.js";
8
8
  import { createConfigRouter } from "./router.js";
9
9
  /** A throwaway dir with no enpilink.config.* so file source never interferes. */
10
10
  let cwd;
11
+ const RESTART_ENV = ["ENPILINK_STORAGE", "ENPILINK_DB_PATH", "PORT"];
11
12
  beforeEach(() => {
12
13
  cwd = fs.mkdtempSync(path.join(os.tmpdir(), "enpi-cfg-"));
14
+ resetBootSnapshotForTests();
13
15
  });
14
16
  afterEach(() => {
15
17
  fs.rmSync(cwd, { recursive: true, force: true });
16
18
  // Clean any env we set.
17
19
  for (const k of Object.keys(process.env)) {
18
- if (k.startsWith("ENPILINK_CFG_") || k === "ENPILINK_ANALYTICS") {
20
+ if (k.startsWith("ENPILINK_CFG_") ||
21
+ k === "ENPILINK_ANALYTICS" ||
22
+ k === "ENPILINK_ADMIN_TOKEN" ||
23
+ k === "ENPILINK_ADMIN" ||
24
+ RESTART_ENV.includes(k)) {
19
25
  delete process.env[k];
20
26
  }
21
27
  }
28
+ resetBootSnapshotForTests();
22
29
  });
23
30
  describe("resolveConfig precedence", () => {
24
31
  it("defaults when no source supplies a value", async () => {
@@ -82,12 +89,20 @@ describe("secret masking", () => {
82
89
  const s = settings.find((x) => x.key === "adminAuthToken");
83
90
  expect(s?.value).toBeNull();
84
91
  });
85
- it("bootstrap keys are always env-locked", async () => {
92
+ it("readonly bootstrap key (admin) is always env-locked", async () => {
86
93
  const { settings } = await resolveConfig(null, cwd);
87
- for (const key of ["storage", "dbPath", "port", "admin"]) {
94
+ const s = settings.find((x) => x.key === "admin");
95
+ expect(s?.tier).toBe("bootstrap");
96
+ expect(s?.editable).toBe("readonly");
97
+ expect(s?.envLocked).toBe(true);
98
+ });
99
+ it("restart-tier bootstrap keys are editable when not env/file-pinned", async () => {
100
+ const { settings } = await resolveConfig(null, cwd);
101
+ for (const key of ["storage", "dbPath", "port"]) {
88
102
  const s = settings.find((x) => x.key === key);
89
103
  expect(s?.tier).toBe("bootstrap");
90
- expect(s?.envLocked).toBe(true);
104
+ expect(s?.editable).toBe("restart");
105
+ expect(s?.envLocked).toBe(false);
91
106
  }
92
107
  });
93
108
  });
@@ -164,11 +179,11 @@ describe("config router", () => {
164
179
  newValue: 0.4,
165
180
  });
166
181
  });
167
- it("PUT rejects a bootstrap key (403)", async () => {
182
+ it("PUT rejects a readonly bootstrap key like `admin` (403)", async () => {
168
183
  const storage = new MemoryStorageAdapter();
169
184
  const app = appWith(storage);
170
- const { status } = await request(app, "PUT", "/__enpilink/config/port", {
171
- value: 9999,
185
+ const { status } = await request(app, "PUT", "/__enpilink/config/admin", {
186
+ value: true,
172
187
  });
173
188
  expect(status).toBe(403);
174
189
  });
@@ -211,4 +226,182 @@ describe("config router", () => {
211
226
  expect(json.audit).toEqual([]);
212
227
  });
213
228
  });
229
+ // --- Per-key metadata (richer ResolvedSetting) ---
230
+ describe("resolved setting metadata", () => {
231
+ it("exposes label/description/group/unit/default/editable per key", async () => {
232
+ const { settings } = await resolveConfig(null, cwd);
233
+ const sample = settings.find((s) => s.key === "analytics.sampleRate");
234
+ expect(sample?.label).toBe("Sampling rate");
235
+ expect(typeof sample?.description).toBe("string");
236
+ expect(sample?.group).toBe("Analytics");
237
+ expect(sample?.unit).toBe("0–1 ratio");
238
+ expect(sample?.default).toBe(1);
239
+ expect(sample?.editable).toBe("runtime");
240
+ });
241
+ it("classifies restart vs readonly editability", async () => {
242
+ const { settings } = await resolveConfig(null, cwd);
243
+ const byKey = new Map(settings.map((s) => [s.key, s]));
244
+ expect(byKey.get("port")?.editable).toBe("restart");
245
+ expect(byKey.get("storage")?.editable).toBe("restart");
246
+ expect(byKey.get("dbPath")?.editable).toBe("restart");
247
+ expect(byKey.get("admin")?.editable).toBe("readonly");
248
+ expect(byKey.get("adminAuthToken")?.editable).toBe("readonly");
249
+ });
250
+ it("restart-tier keys NOT pinned by env/file are not env-locked", async () => {
251
+ const { settings } = await resolveConfig(null, cwd);
252
+ const port = settings.find((s) => s.key === "port");
253
+ expect(port?.envLocked).toBe(false);
254
+ const admin = settings.find((s) => s.key === "admin");
255
+ // readonly key stays env-locked (read-only) even without an env pin.
256
+ expect(admin?.envLocked).toBe(true);
257
+ });
258
+ it("modified=true only when a DB override differs from the default", async () => {
259
+ const storage = new MemoryStorageAdapter();
260
+ await storage.setConfig("retention.events", 9999);
261
+ const { settings } = await resolveConfig(storage, cwd);
262
+ const s = settings.find((x) => x.key === "retention.events");
263
+ expect(s?.modified).toBe(true);
264
+ // an untouched key is not modified
265
+ const other = settings.find((x) => x.key === "analytics.sampleRate");
266
+ expect(other?.modified).toBe(false);
267
+ });
268
+ });
269
+ // --- Restart-tier writes + restartRequired ---
270
+ describe("restart-tier editability", () => {
271
+ it("validateConfigWrite accepts a restart key, rejects secret", () => {
272
+ expect(validateConfigWrite("port", 8080).ok).toBe(true);
273
+ expect(validateConfigWrite("storage", "sqlite").ok).toBe(true);
274
+ expect(validateConfigWrite("adminAuthToken", "x").ok).toBe(false);
275
+ expect(validateConfigWrite("admin", true).ok).toBe(false);
276
+ });
277
+ it("PUT a restart key persists + flags restartRequired", async () => {
278
+ const storage = new MemoryStorageAdapter();
279
+ const app = appWith(storage);
280
+ const { status, json } = await request(app, "PUT", "/__enpilink/config/port", {
281
+ value: 8080,
282
+ });
283
+ expect(status).toBe(200);
284
+ expect(json.restartRequired).toBe(true);
285
+ expect(await storage.getConfig("port")).toBe(8080);
286
+ // The boot snapshot is the default 3000; the persisted 8080 differs → pending.
287
+ const { settings } = await resolveConfig(storage, cwd);
288
+ const port = settings.find((s) => s.key === "port");
289
+ expect(port?.source).toBe("db");
290
+ expect(port?.value).toBe(8080);
291
+ expect(port?.restartRequired).toBe(true);
292
+ });
293
+ it("restartRequired is false when the DB value equals the booted value", async () => {
294
+ const storage = new MemoryStorageAdapter();
295
+ // booted with default 3000; persist the same value
296
+ await storage.setConfig("port", 3000);
297
+ const { settings } = await resolveConfig(storage, cwd);
298
+ const port = settings.find((s) => s.key === "port");
299
+ expect(port?.restartRequired).toBe(false);
300
+ });
301
+ it("PUT a restart key 409s when env-pinned (envLocked)", async () => {
302
+ process.env.PORT = "4000";
303
+ resetBootSnapshotForTests();
304
+ const storage = new MemoryStorageAdapter();
305
+ const app = appWith(storage);
306
+ const { status } = await request(app, "PUT", "/__enpilink/config/port", {
307
+ value: 8080,
308
+ });
309
+ expect(status).toBe(409);
310
+ });
311
+ });
312
+ // --- Security guardrails ---
313
+ describe("write guardrails (PUT + DELETE)", () => {
314
+ it("PUT rejects `admin` (403) — never web-editable", async () => {
315
+ const app = appWith(new MemoryStorageAdapter());
316
+ const { status } = await request(app, "PUT", "/__enpilink/config/admin", {
317
+ value: true,
318
+ });
319
+ expect(status).toBe(403);
320
+ });
321
+ it("PUT rejects `adminAuthToken` (403) — secret", async () => {
322
+ const app = appWith(new MemoryStorageAdapter());
323
+ const { status } = await request(app, "PUT", "/__enpilink/config/adminAuthToken", { value: "leak" });
324
+ expect(status).toBe(403);
325
+ });
326
+ it("PUT rejects an unknown key (404)", async () => {
327
+ const app = appWith(new MemoryStorageAdapter());
328
+ const { status } = await request(app, "PUT", "/__enpilink/config/nope", {
329
+ value: 1,
330
+ });
331
+ expect(status).toBe(404);
332
+ });
333
+ it("PUT rejects an env-locked runtime key (409)", async () => {
334
+ process.env.ENPILINK_CFG_FLAGS_LIVE_LOGS = "false";
335
+ const app = appWith(new MemoryStorageAdapter());
336
+ const { status } = await request(app, "PUT", "/__enpilink/config/flags.liveLogs", { value: true });
337
+ expect(status).toBe(409);
338
+ });
339
+ it("DELETE rejects `admin`/`adminAuthToken`/unknown the same way", async () => {
340
+ const app = appWith(new MemoryStorageAdapter());
341
+ expect((await request(app, "DELETE", "/__enpilink/config/admin")).status).toBe(403);
342
+ expect((await request(app, "DELETE", "/__enpilink/config/adminAuthToken"))
343
+ .status).toBe(403);
344
+ expect((await request(app, "DELETE", "/__enpilink/config/nope")).status).toBe(404);
345
+ });
346
+ });
347
+ // --- Reset to default ---
348
+ describe("reset to default (DELETE)", () => {
349
+ it("clears a DB override and falls back to default + audits", async () => {
350
+ const storage = new MemoryStorageAdapter();
351
+ await storage.setConfig("retention.events", 9999, "tester");
352
+ const app = appWith(storage);
353
+ const { status, json } = await request(app, "DELETE", "/__enpilink/config/retention.events");
354
+ expect(status).toBe(200);
355
+ expect(json.reset).toBe(true);
356
+ // Override gone → resolution falls back to the default.
357
+ const { settings } = await resolveConfig(storage, cwd);
358
+ const s = settings.find((x) => x.key === "retention.events");
359
+ expect(s?.source).toBe("default");
360
+ expect(s?.value).toBe(5000);
361
+ expect(s?.modified).toBe(false);
362
+ // Audit recorded the reset (most recent first; router actor = "dev").
363
+ const audit = await storage.getConfigAudit();
364
+ expect(audit[0]).toMatchObject({ key: "retention.events", actor: "dev" });
365
+ });
366
+ it("DELETE with no storage → 409", async () => {
367
+ const app = appWith(null);
368
+ const { status } = await request(app, "DELETE", "/__enpilink/config/flags.liveLogs");
369
+ expect(status).toBe(409);
370
+ });
371
+ });
372
+ // --- Presets ---
373
+ describe("presets", () => {
374
+ it("GET /config/presets lists Dev + Prod with values", async () => {
375
+ const app = appWith(new MemoryStorageAdapter());
376
+ const { status, json } = await request(app, "GET", "/__enpilink/config/presets");
377
+ expect(status).toBe(200);
378
+ const presets = json.presets;
379
+ expect(presets.map((p) => p.name).sort()).toEqual(["dev", "prod"]);
380
+ });
381
+ it("POST applies a preset to runtime keys + audits", async () => {
382
+ const storage = new MemoryStorageAdapter();
383
+ const app = appWith(storage);
384
+ const { status, json } = await request(app, "POST", "/__enpilink/config/preset/prod");
385
+ expect(status).toBe(200);
386
+ const body = json;
387
+ expect(body.applied.length).toBeGreaterThan(0);
388
+ expect(await storage.getConfig("analytics.sampleRate")).toBe(0.25);
389
+ expect(await storage.getConfig("flags.liveLogs")).toBe(false);
390
+ });
391
+ it("POST skips env-locked keys, reporting them", async () => {
392
+ process.env.ENPILINK_CFG_ANALYTICS_SAMPLE_RATE = "1";
393
+ const storage = new MemoryStorageAdapter();
394
+ const app = appWith(storage);
395
+ const { json } = await request(app, "POST", "/__enpilink/config/preset/prod");
396
+ const body = json;
397
+ expect(body.skipped.some((s) => s.key === "analytics.sampleRate")).toBe(true);
398
+ // env-pinned key was NOT persisted
399
+ expect(await storage.getConfig("analytics.sampleRate")).toBeUndefined();
400
+ });
401
+ it("POST unknown preset → 404", async () => {
402
+ const app = appWith(new MemoryStorageAdapter());
403
+ const { status } = await request(app, "POST", "/__enpilink/config/preset/nope");
404
+ expect(status).toBe(404);
405
+ });
406
+ });
214
407
  //# sourceMappingURL=config.test.js.map
@@ -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 { type ConfigSource, loadConfigFile, MASKED, type ResolvedConfig, type ResolvedSetting, resolveConfig, validateRuntimeWrite, } from "./resolve.js";
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 { loadConfigFile, MASKED, resolveConfig, validateRuntimeWrite, } from "./resolve.js";
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 read-only in the admin UI. True for all bootstrap keys,
30
- * and for any runtime key pinned via env or file (the DB value is shadowed).
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
+ };