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
@@ -43,7 +43,16 @@ export type Config = z.infer<typeof configSchema>;
43
43
  export type ConfigKey = keyof Config;
44
44
  export type BootstrapKey = keyof BootstrapConfig;
45
45
  export type RuntimeKey = keyof RuntimeConfig;
46
- /** Per-key metadata describing tier / secret / env-lock semantics. */
46
+ /**
47
+ * Editability classification, surfaced to the admin UI:
48
+ * - `runtime` — editable live; takes effect immediately.
49
+ * - `restart` — DB-editable but only takes effect after a process restart
50
+ * (the non-secret bootstrap keys `port`/`storage`/`dbPath`).
51
+ * - `readonly` — env-only; never web-editable (the `admin` gate + the
52
+ * `adminAuthToken` secret).
53
+ */
54
+ export type Editable = "runtime" | "restart" | "readonly";
55
+ /** Per-key metadata describing tier / secret / env-lock + UI presentation. */
47
56
  export interface KeyMeta {
48
57
  key: ConfigKey;
49
58
  /** `bootstrap` (env/file only) or `runtime` (DB-editable). */
@@ -52,6 +61,18 @@ export interface KeyMeta {
52
61
  secret: boolean;
53
62
  /** The env var that drives this key (for the "set via env" hint). */
54
63
  env: string;
64
+ /** Human-friendly label (primary heading in the UI). */
65
+ label: string;
66
+ /** One-line plain-language description of what the setting does. */
67
+ description: string;
68
+ /** Functional category used to group settings in the UI. */
69
+ group: string;
70
+ /** Optional unit hint (e.g. `"ms"`, `"events"`, `"0–1 ratio"`). */
71
+ unit?: string;
72
+ /** The schema default value for this key. */
73
+ default: unknown;
74
+ /** How this key may be edited from the admin UI. */
75
+ editable: Editable;
55
76
  }
56
77
  /** Bootstrap keys (env/file only). */
57
78
  export declare const BOOTSTRAP_KEYS: readonly ["storage", "dbPath", "port", "admin", "adminAuthToken"];
@@ -59,6 +80,12 @@ export declare const BOOTSTRAP_KEYS: readonly ["storage", "dbPath", "port", "adm
59
80
  export declare const RUNTIME_KEYS: readonly ["analytics.enabled", "analytics.sampleRate", "retention.events", "retention.logs", "flags.liveLogs", "display.bucketMs"];
60
81
  /** Secret keys: env-only, masked + never persisted/returned in plaintext. */
61
82
  export declare const SECRET_KEYS: readonly ["adminAuthToken"];
83
+ /**
84
+ * Restart-tier keys: non-secret bootstrap keys that ARE DB-editable but only
85
+ * take effect after a process restart. Resolution still honours env>file>db so
86
+ * an env/file pin locks them (read-only).
87
+ */
88
+ export declare const RESTART_KEYS: readonly ["port", "storage", "dbPath"];
62
89
  /**
63
90
  * Env var mapping per key. Bootstrap keys map to dedicated env vars that match
64
91
  * the framework's existing env surface; runtime keys use an
@@ -70,6 +97,17 @@ export declare function isSecretKey(key: string): boolean;
70
97
  export declare function isRuntimeKey(key: string): key is RuntimeKey;
71
98
  export declare function isBootstrapKey(key: string): key is BootstrapKey;
72
99
  export declare function isKnownKey(key: string): key is ConfigKey;
100
+ /** A restart-tier key: DB-editable but only effective after a restart. */
101
+ export declare function isRestartKey(key: string): key is BootstrapKey;
102
+ /**
103
+ * The editability classification for a key:
104
+ * - secret/admin → `readonly` (env-only, never web-editable)
105
+ * - other bootstrap keys (port/storage/dbPath) → `restart`
106
+ * - runtime keys → `runtime`
107
+ */
108
+ export declare function editableOf(key: ConfigKey): Editable;
109
+ /** The schema default value for a key (undefined for optional secrets). */
110
+ export declare function defaultForKey(key: ConfigKey): unknown;
73
111
  /** Metadata for every known key. */
74
112
  export declare function keyMeta(key: ConfigKey): KeyMeta;
75
113
  /** All keys with metadata. */
@@ -83,6 +83,84 @@ export const runtimeSchema = z.object({
83
83
  "display.bucketMs": z.number().int().positive().default(60_000),
84
84
  });
85
85
  export const configSchema = bootstrapSchema.merge(runtimeSchema);
86
+ const KEY_DESCRIPTORS = {
87
+ // --- Server (restart-tier bootstrap) ---
88
+ port: {
89
+ label: "Server port",
90
+ description: "The network port the server listens on. Changing this needs a restart to take effect.",
91
+ group: "Server",
92
+ editable: "restart",
93
+ },
94
+ storage: {
95
+ label: "Storage engine",
96
+ description: "Where analytics, logs, and settings are persisted: in-memory (resets on restart), sqlite (a local file), or postgres. Takes effect after a restart.",
97
+ group: "Storage",
98
+ editable: "restart",
99
+ },
100
+ dbPath: {
101
+ label: "SQLite database file",
102
+ description: "Path to the SQLite database file (only used when the storage engine is sqlite). Takes effect after a restart.",
103
+ group: "Storage",
104
+ editable: "restart",
105
+ },
106
+ // --- Security (read-only, env-only) ---
107
+ admin: {
108
+ label: "Production admin plane",
109
+ description: "Enables the admin dashboard in production. For safety this can only be turned on via environment variable, never from the web UI.",
110
+ group: "Security",
111
+ editable: "readonly",
112
+ },
113
+ adminAuthToken: {
114
+ label: "Admin auth token",
115
+ description: "Secret bearer token guarding the production admin plane. Set via environment only; never stored or shown in plaintext.",
116
+ group: "Security",
117
+ editable: "readonly",
118
+ },
119
+ // --- Analytics (runtime) ---
120
+ "analytics.enabled": {
121
+ label: "Analytics enabled",
122
+ description: "Record tool-call events and server logs so the dashboard can show usage and latency.",
123
+ group: "Analytics",
124
+ editable: "runtime",
125
+ },
126
+ "analytics.sampleRate": {
127
+ label: "Sampling rate",
128
+ description: "Fraction of requests to record. 1 records everything; lower values reduce overhead and storage on busy servers.",
129
+ group: "Analytics",
130
+ unit: "0–1 ratio",
131
+ editable: "runtime",
132
+ },
133
+ // --- Retention (runtime) ---
134
+ "retention.events": {
135
+ label: "Event retention",
136
+ description: "Maximum number of tool-call events kept. Oldest events are dropped once the cap is reached.",
137
+ group: "Retention",
138
+ unit: "events",
139
+ editable: "runtime",
140
+ },
141
+ "retention.logs": {
142
+ label: "Log retention",
143
+ description: "Maximum number of captured log lines kept. Oldest logs are dropped once the cap is reached.",
144
+ group: "Retention",
145
+ unit: "logs",
146
+ editable: "runtime",
147
+ },
148
+ // --- Features (runtime) ---
149
+ "flags.liveLogs": {
150
+ label: "Live log stream",
151
+ description: "Show the real-time server log stream in the dashboard's live-logs panel.",
152
+ group: "Features",
153
+ editable: "runtime",
154
+ },
155
+ // --- Display (runtime) ---
156
+ "display.bucketMs": {
157
+ label: "Chart time bucket",
158
+ description: "Default width of each time bucket in the dashboard's volume/latency charts.",
159
+ group: "Display",
160
+ unit: "ms",
161
+ editable: "runtime",
162
+ },
163
+ };
86
164
  /** Bootstrap keys (env/file only). */
87
165
  export const BOOTSTRAP_KEYS = [
88
166
  "storage",
@@ -104,6 +182,16 @@ export const RUNTIME_KEYS = [
104
182
  export const SECRET_KEYS = [
105
183
  "adminAuthToken",
106
184
  ];
185
+ /**
186
+ * Restart-tier keys: non-secret bootstrap keys that ARE DB-editable but only
187
+ * take effect after a process restart. Resolution still honours env>file>db so
188
+ * an env/file pin locks them (read-only).
189
+ */
190
+ export const RESTART_KEYS = [
191
+ "port",
192
+ "storage",
193
+ "dbPath",
194
+ ];
107
195
  /**
108
196
  * Env var mapping per key. Bootstrap keys map to dedicated env vars that match
109
197
  * the framework's existing env surface; runtime keys use an
@@ -126,6 +214,7 @@ export const ENV_VARS = {
126
214
  const SECRET_SET = new Set(SECRET_KEYS);
127
215
  const RUNTIME_SET = new Set(RUNTIME_KEYS);
128
216
  const BOOTSTRAP_SET = new Set(BOOTSTRAP_KEYS);
217
+ const RESTART_SET = new Set(RESTART_KEYS);
129
218
  export function isSecretKey(key) {
130
219
  return SECRET_SET.has(key);
131
220
  }
@@ -138,13 +227,45 @@ export function isBootstrapKey(key) {
138
227
  export function isKnownKey(key) {
139
228
  return RUNTIME_SET.has(key) || BOOTSTRAP_SET.has(key);
140
229
  }
230
+ /** A restart-tier key: DB-editable but only effective after a restart. */
231
+ export function isRestartKey(key) {
232
+ return RESTART_SET.has(key);
233
+ }
234
+ /**
235
+ * The editability classification for a key:
236
+ * - secret/admin → `readonly` (env-only, never web-editable)
237
+ * - other bootstrap keys (port/storage/dbPath) → `restart`
238
+ * - runtime keys → `runtime`
239
+ */
240
+ export function editableOf(key) {
241
+ if (isRestartKey(key)) {
242
+ return "restart";
243
+ }
244
+ if (isBootstrapKey(key)) {
245
+ return "readonly";
246
+ }
247
+ return "runtime";
248
+ }
249
+ /** Default-typed sample used to read each key's schema default. */
250
+ const DEFAULTS = configSchema.parse({});
251
+ /** The schema default value for a key (undefined for optional secrets). */
252
+ export function defaultForKey(key) {
253
+ return DEFAULTS[key];
254
+ }
141
255
  /** Metadata for every known key. */
142
256
  export function keyMeta(key) {
257
+ const d = KEY_DESCRIPTORS[key];
143
258
  return {
144
259
  key,
145
260
  tier: isBootstrapKey(key) ? "bootstrap" : "runtime",
146
261
  secret: isSecretKey(key),
147
262
  env: ENV_VARS[key],
263
+ label: d.label,
264
+ description: d.description,
265
+ group: d.group,
266
+ unit: d.unit,
267
+ default: defaultForKey(key),
268
+ editable: d.editable,
148
269
  };
149
270
  }
150
271
  /** All keys with metadata. */
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/server/config/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Config schema for enpilink's admin / control plane (M4).\n *\n * Settings are split into two tiers:\n *\n * - **Bootstrap** keys are env/file ONLY — they configure how the process\n * starts (storage engine, port, whether the prod admin is enabled, the admin\n * auth secret). They are NOT editable from the DB / admin UI. Some are\n * **secret** (`adminAuthToken`) and are never persisted to the DB nor\n * returned in plaintext by any API.\n * - **Runtime** keys live in the DB and are editable from the Configuration\n * admin page (analytics on/off + sample rate, retention, feature flags,\n * display prefs). They still honour the env > file > db precedence so an\n * operator can pin a value via env/file and lock it in the UI.\n *\n * Resolution precedence is **env > file > db > default** (see `resolve.ts`).\n */\n\n/** A boolean parsed from an env string: `1`/`true`/`yes`/`on` (ci) → true. */\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"]);\nconst FALSY = new Set([\"0\", \"false\", \"no\", \"off\"]);\n\n/** Coerce an env string (or already-typed value) into a boolean. */\nexport function coerceBool(raw: unknown): boolean | undefined {\n if (typeof raw === \"boolean\") {\n return raw;\n }\n if (typeof raw === \"string\") {\n const v = raw.trim().toLowerCase();\n if (TRUTHY.has(v)) {\n return true;\n }\n if (FALSY.has(v)) {\n return false;\n }\n }\n return undefined;\n}\n\n/** Coerce an env string (or number) into a finite number. */\nexport function coerceNumber(raw: unknown): number | undefined {\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return raw;\n }\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n const n = Number(raw);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n return undefined;\n}\n\n/**\n * Bootstrap (env/file-only) settings. Not DB-editable. Defaults keep the\n * framework off-by-default and secure-by-default.\n */\nexport const bootstrapSchema = z.object({\n /** Storage engine name (`memory` | `sqlite` | custom). */\n storage: z.string().default(\"memory\"),\n /** SQLite database path (only meaningful for the sqlite engine). */\n dbPath: z.string().default(\"./enpilink.db\"),\n /** HTTP port the server listens on. */\n port: z.number().int().positive().default(3000),\n /** Whether the production admin plane is enabled (M5). Off by default. */\n admin: z.boolean().default(false),\n /**\n * Bearer token guarding the prod admin plane (M5). SECRET — env-only, never\n * persisted to the DB nor returned in plaintext.\n */\n adminAuthToken: z.string().optional(),\n});\n\n/** Runtime (DB-editable) settings. */\nexport const runtimeSchema = z.object({\n /** Whether analytics/event capture is enabled. */\n \"analytics.enabled\": z.boolean().default(false),\n /** Fraction of requests sampled `[0, 1]`. */\n \"analytics.sampleRate\": z.number().min(0).max(1).default(1),\n /** Max events retained (retention cap). */\n \"retention.events\": z.number().int().nonnegative().default(5000),\n /** Max logs retained (retention cap). */\n \"retention.logs\": z.number().int().nonnegative().default(5000),\n /** Feature flag: expose the live log stream in the dashboard. */\n \"flags.liveLogs\": z.boolean().default(true),\n /** Display preference: dashboard time-bucket width (ms). */\n \"display.bucketMs\": z.number().int().positive().default(60_000),\n});\n\nexport const configSchema = bootstrapSchema.merge(runtimeSchema);\n\nexport type BootstrapConfig = z.infer<typeof bootstrapSchema>;\nexport type RuntimeConfig = z.infer<typeof runtimeSchema>;\nexport type Config = z.infer<typeof configSchema>;\n\n/** All known config keys. */\nexport type ConfigKey = keyof Config;\nexport type BootstrapKey = keyof BootstrapConfig;\nexport type RuntimeKey = keyof RuntimeConfig;\n\n/** Per-key metadata describing tier / secret / env-lock semantics. */\nexport interface KeyMeta {\n key: ConfigKey;\n /** `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** Secret keys are env-only and NEVER persisted/returned in plaintext. */\n secret: boolean;\n /** The env var that drives this key (for the \"set via env\" hint). */\n env: string;\n}\n\n/** Bootstrap keys (env/file only). */\nexport const BOOTSTRAP_KEYS = [\n \"storage\",\n \"dbPath\",\n \"port\",\n \"admin\",\n \"adminAuthToken\",\n] as const satisfies readonly BootstrapKey[];\n\n/** Runtime keys (DB-editable). */\nexport const RUNTIME_KEYS = [\n \"analytics.enabled\",\n \"analytics.sampleRate\",\n \"retention.events\",\n \"retention.logs\",\n \"flags.liveLogs\",\n \"display.bucketMs\",\n] as const satisfies readonly RuntimeKey[];\n\n/** Secret keys: env-only, masked + never persisted/returned in plaintext. */\nexport const SECRET_KEYS = [\n \"adminAuthToken\",\n] as const satisfies readonly ConfigKey[];\n\n/**\n * Env var mapping per key. Bootstrap keys map to dedicated env vars that match\n * the framework's existing env surface; runtime keys use an\n * `ENPILINK_CFG_<KEY>` convention so an operator can pin (env-lock) any runtime\n * value without colliding with the bootstrap vars.\n */\nexport const ENV_VARS: Record<ConfigKey, string> = {\n storage: \"ENPILINK_STORAGE\",\n dbPath: \"ENPILINK_DB_PATH\",\n port: \"PORT\",\n admin: \"ENPILINK_ADMIN\",\n adminAuthToken: \"ENPILINK_ADMIN_TOKEN\",\n \"analytics.enabled\": \"ENPILINK_ANALYTICS\",\n \"analytics.sampleRate\": \"ENPILINK_CFG_ANALYTICS_SAMPLE_RATE\",\n \"retention.events\": \"ENPILINK_CFG_RETENTION_EVENTS\",\n \"retention.logs\": \"ENPILINK_CFG_RETENTION_LOGS\",\n \"flags.liveLogs\": \"ENPILINK_CFG_FLAGS_LIVE_LOGS\",\n \"display.bucketMs\": \"ENPILINK_CFG_DISPLAY_BUCKET_MS\",\n};\n\nconst SECRET_SET = new Set<string>(SECRET_KEYS);\nconst RUNTIME_SET = new Set<string>(RUNTIME_KEYS);\nconst BOOTSTRAP_SET = new Set<string>(BOOTSTRAP_KEYS);\n\nexport function isSecretKey(key: string): boolean {\n return SECRET_SET.has(key);\n}\nexport function isRuntimeKey(key: string): key is RuntimeKey {\n return RUNTIME_SET.has(key);\n}\nexport function isBootstrapKey(key: string): key is BootstrapKey {\n return BOOTSTRAP_SET.has(key);\n}\nexport function isKnownKey(key: string): key is ConfigKey {\n return RUNTIME_SET.has(key) || BOOTSTRAP_SET.has(key);\n}\n\n/** Metadata for every known key. */\nexport function keyMeta(key: ConfigKey): KeyMeta {\n return {\n key,\n tier: isBootstrapKey(key) ? \"bootstrap\" : \"runtime\",\n secret: isSecretKey(key),\n env: ENV_VARS[key],\n };\n}\n\n/** All keys with metadata. */\nexport function allKeyMeta(): KeyMeta[] {\n return [...BOOTSTRAP_KEYS, ...RUNTIME_KEYS].map((k) => keyMeta(k));\n}\n\n/** The per-key zod schema (for validating a single runtime write). */\nexport function schemaForKey(key: ConfigKey): z.ZodTypeAny {\n return (\n (configSchema.shape as Record<string, z.ZodTypeAny>)[key] ?? z.unknown()\n );\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/server/config/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Config schema for enpilink's admin / control plane (M4).\n *\n * Settings are split into two tiers:\n *\n * - **Bootstrap** keys are env/file ONLY — they configure how the process\n * starts (storage engine, port, whether the prod admin is enabled, the admin\n * auth secret). They are NOT editable from the DB / admin UI. Some are\n * **secret** (`adminAuthToken`) and are never persisted to the DB nor\n * returned in plaintext by any API.\n * - **Runtime** keys live in the DB and are editable from the Configuration\n * admin page (analytics on/off + sample rate, retention, feature flags,\n * display prefs). They still honour the env > file > db precedence so an\n * operator can pin a value via env/file and lock it in the UI.\n *\n * Resolution precedence is **env > file > db > default** (see `resolve.ts`).\n */\n\n/** A boolean parsed from an env string: `1`/`true`/`yes`/`on` (ci) → true. */\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"]);\nconst FALSY = new Set([\"0\", \"false\", \"no\", \"off\"]);\n\n/** Coerce an env string (or already-typed value) into a boolean. */\nexport function coerceBool(raw: unknown): boolean | undefined {\n if (typeof raw === \"boolean\") {\n return raw;\n }\n if (typeof raw === \"string\") {\n const v = raw.trim().toLowerCase();\n if (TRUTHY.has(v)) {\n return true;\n }\n if (FALSY.has(v)) {\n return false;\n }\n }\n return undefined;\n}\n\n/** Coerce an env string (or number) into a finite number. */\nexport function coerceNumber(raw: unknown): number | undefined {\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return raw;\n }\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n const n = Number(raw);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n return undefined;\n}\n\n/**\n * Bootstrap (env/file-only) settings. Not DB-editable. Defaults keep the\n * framework off-by-default and secure-by-default.\n */\nexport const bootstrapSchema = z.object({\n /** Storage engine name (`memory` | `sqlite` | custom). */\n storage: z.string().default(\"memory\"),\n /** SQLite database path (only meaningful for the sqlite engine). */\n dbPath: z.string().default(\"./enpilink.db\"),\n /** HTTP port the server listens on. */\n port: z.number().int().positive().default(3000),\n /** Whether the production admin plane is enabled (M5). Off by default. */\n admin: z.boolean().default(false),\n /**\n * Bearer token guarding the prod admin plane (M5). SECRET — env-only, never\n * persisted to the DB nor returned in plaintext.\n */\n adminAuthToken: z.string().optional(),\n});\n\n/** Runtime (DB-editable) settings. */\nexport const runtimeSchema = z.object({\n /** Whether analytics/event capture is enabled. */\n \"analytics.enabled\": z.boolean().default(false),\n /** Fraction of requests sampled `[0, 1]`. */\n \"analytics.sampleRate\": z.number().min(0).max(1).default(1),\n /** Max events retained (retention cap). */\n \"retention.events\": z.number().int().nonnegative().default(5000),\n /** Max logs retained (retention cap). */\n \"retention.logs\": z.number().int().nonnegative().default(5000),\n /** Feature flag: expose the live log stream in the dashboard. */\n \"flags.liveLogs\": z.boolean().default(true),\n /** Display preference: dashboard time-bucket width (ms). */\n \"display.bucketMs\": z.number().int().positive().default(60_000),\n});\n\nexport const configSchema = bootstrapSchema.merge(runtimeSchema);\n\nexport type BootstrapConfig = z.infer<typeof bootstrapSchema>;\nexport type RuntimeConfig = z.infer<typeof runtimeSchema>;\nexport type Config = z.infer<typeof configSchema>;\n\n/** All known config keys. */\nexport type ConfigKey = keyof Config;\nexport type BootstrapKey = keyof BootstrapConfig;\nexport type RuntimeKey = keyof RuntimeConfig;\n\n/**\n * Editability classification, surfaced to the admin UI:\n * - `runtime` — editable live; takes effect immediately.\n * - `restart` — DB-editable but only takes effect after a process restart\n * (the non-secret bootstrap keys `port`/`storage`/`dbPath`).\n * - `readonly` — env-only; never web-editable (the `admin` gate + the\n * `adminAuthToken` secret).\n */\nexport type Editable = \"runtime\" | \"restart\" | \"readonly\";\n\n/** Per-key metadata describing tier / secret / env-lock + UI presentation. */\nexport interface KeyMeta {\n key: ConfigKey;\n /** `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** Secret keys are env-only and NEVER persisted/returned in plaintext. */\n secret: boolean;\n /** The env var that drives this key (for the \"set via env\" hint). */\n env: string;\n /** Human-friendly label (primary heading in the UI). */\n label: string;\n /** One-line plain-language description of what the setting does. */\n description: string;\n /** Functional category used to group settings in the UI. */\n group: string;\n /** Optional unit hint (e.g. `\"ms\"`, `\"events\"`, `\"0–1 ratio\"`). */\n unit?: string;\n /** The schema default value for this key. */\n default: unknown;\n /** How this key may be edited from the admin UI. */\n editable: Editable;\n}\n\n/**\n * Per-key UI/editability descriptors. `label`/`description`/`group`/`unit` are\n * user-facing — kept clear and non-jargon. `editable` drives the three-tier\n * editability story (runtime live · restart-required · env-only read-only).\n */\ninterface KeyDescriptor {\n label: string;\n description: string;\n group: string;\n unit?: string;\n editable: Editable;\n}\n\nconst KEY_DESCRIPTORS: Record<ConfigKey, KeyDescriptor> = {\n // --- Server (restart-tier bootstrap) ---\n port: {\n label: \"Server port\",\n description:\n \"The network port the server listens on. Changing this needs a restart to take effect.\",\n group: \"Server\",\n editable: \"restart\",\n },\n storage: {\n label: \"Storage engine\",\n description:\n \"Where analytics, logs, and settings are persisted: in-memory (resets on restart), sqlite (a local file), or postgres. Takes effect after a restart.\",\n group: \"Storage\",\n editable: \"restart\",\n },\n dbPath: {\n label: \"SQLite database file\",\n description:\n \"Path to the SQLite database file (only used when the storage engine is sqlite). Takes effect after a restart.\",\n group: \"Storage\",\n editable: \"restart\",\n },\n // --- Security (read-only, env-only) ---\n admin: {\n label: \"Production admin plane\",\n description:\n \"Enables the admin dashboard in production. For safety this can only be turned on via environment variable, never from the web UI.\",\n group: \"Security\",\n editable: \"readonly\",\n },\n adminAuthToken: {\n label: \"Admin auth token\",\n description:\n \"Secret bearer token guarding the production admin plane. Set via environment only; never stored or shown in plaintext.\",\n group: \"Security\",\n editable: \"readonly\",\n },\n // --- Analytics (runtime) ---\n \"analytics.enabled\": {\n label: \"Analytics enabled\",\n description:\n \"Record tool-call events and server logs so the dashboard can show usage and latency.\",\n group: \"Analytics\",\n editable: \"runtime\",\n },\n \"analytics.sampleRate\": {\n label: \"Sampling rate\",\n description:\n \"Fraction of requests to record. 1 records everything; lower values reduce overhead and storage on busy servers.\",\n group: \"Analytics\",\n unit: \"0–1 ratio\",\n editable: \"runtime\",\n },\n // --- Retention (runtime) ---\n \"retention.events\": {\n label: \"Event retention\",\n description:\n \"Maximum number of tool-call events kept. Oldest events are dropped once the cap is reached.\",\n group: \"Retention\",\n unit: \"events\",\n editable: \"runtime\",\n },\n \"retention.logs\": {\n label: \"Log retention\",\n description:\n \"Maximum number of captured log lines kept. Oldest logs are dropped once the cap is reached.\",\n group: \"Retention\",\n unit: \"logs\",\n editable: \"runtime\",\n },\n // --- Features (runtime) ---\n \"flags.liveLogs\": {\n label: \"Live log stream\",\n description:\n \"Show the real-time server log stream in the dashboard's live-logs panel.\",\n group: \"Features\",\n editable: \"runtime\",\n },\n // --- Display (runtime) ---\n \"display.bucketMs\": {\n label: \"Chart time bucket\",\n description:\n \"Default width of each time bucket in the dashboard's volume/latency charts.\",\n group: \"Display\",\n unit: \"ms\",\n editable: \"runtime\",\n },\n};\n\n/** Bootstrap keys (env/file only). */\nexport const BOOTSTRAP_KEYS = [\n \"storage\",\n \"dbPath\",\n \"port\",\n \"admin\",\n \"adminAuthToken\",\n] as const satisfies readonly BootstrapKey[];\n\n/** Runtime keys (DB-editable). */\nexport const RUNTIME_KEYS = [\n \"analytics.enabled\",\n \"analytics.sampleRate\",\n \"retention.events\",\n \"retention.logs\",\n \"flags.liveLogs\",\n \"display.bucketMs\",\n] as const satisfies readonly RuntimeKey[];\n\n/** Secret keys: env-only, masked + never persisted/returned in plaintext. */\nexport const SECRET_KEYS = [\n \"adminAuthToken\",\n] as const satisfies readonly ConfigKey[];\n\n/**\n * Restart-tier keys: non-secret bootstrap keys that ARE DB-editable but only\n * take effect after a process restart. Resolution still honours env>file>db so\n * an env/file pin locks them (read-only).\n */\nexport const RESTART_KEYS = [\n \"port\",\n \"storage\",\n \"dbPath\",\n] as const satisfies readonly BootstrapKey[];\n\n/**\n * Env var mapping per key. Bootstrap keys map to dedicated env vars that match\n * the framework's existing env surface; runtime keys use an\n * `ENPILINK_CFG_<KEY>` convention so an operator can pin (env-lock) any runtime\n * value without colliding with the bootstrap vars.\n */\nexport const ENV_VARS: Record<ConfigKey, string> = {\n storage: \"ENPILINK_STORAGE\",\n dbPath: \"ENPILINK_DB_PATH\",\n port: \"PORT\",\n admin: \"ENPILINK_ADMIN\",\n adminAuthToken: \"ENPILINK_ADMIN_TOKEN\",\n \"analytics.enabled\": \"ENPILINK_ANALYTICS\",\n \"analytics.sampleRate\": \"ENPILINK_CFG_ANALYTICS_SAMPLE_RATE\",\n \"retention.events\": \"ENPILINK_CFG_RETENTION_EVENTS\",\n \"retention.logs\": \"ENPILINK_CFG_RETENTION_LOGS\",\n \"flags.liveLogs\": \"ENPILINK_CFG_FLAGS_LIVE_LOGS\",\n \"display.bucketMs\": \"ENPILINK_CFG_DISPLAY_BUCKET_MS\",\n};\n\nconst SECRET_SET = new Set<string>(SECRET_KEYS);\nconst RUNTIME_SET = new Set<string>(RUNTIME_KEYS);\nconst BOOTSTRAP_SET = new Set<string>(BOOTSTRAP_KEYS);\nconst RESTART_SET = new Set<string>(RESTART_KEYS);\n\nexport function isSecretKey(key: string): boolean {\n return SECRET_SET.has(key);\n}\nexport function isRuntimeKey(key: string): key is RuntimeKey {\n return RUNTIME_SET.has(key);\n}\nexport function isBootstrapKey(key: string): key is BootstrapKey {\n return BOOTSTRAP_SET.has(key);\n}\nexport function isKnownKey(key: string): key is ConfigKey {\n return RUNTIME_SET.has(key) || BOOTSTRAP_SET.has(key);\n}\n/** A restart-tier key: DB-editable but only effective after a restart. */\nexport function isRestartKey(key: string): key is BootstrapKey {\n return RESTART_SET.has(key);\n}\n\n/**\n * The editability classification for a key:\n * - secret/admin → `readonly` (env-only, never web-editable)\n * - other bootstrap keys (port/storage/dbPath) → `restart`\n * - runtime keys → `runtime`\n */\nexport function editableOf(key: ConfigKey): Editable {\n if (isRestartKey(key)) {\n return \"restart\";\n }\n if (isBootstrapKey(key)) {\n return \"readonly\";\n }\n return \"runtime\";\n}\n\n/** Default-typed sample used to read each key's schema default. */\nconst DEFAULTS = configSchema.parse({}) as Record<string, unknown>;\n\n/** The schema default value for a key (undefined for optional secrets). */\nexport function defaultForKey(key: ConfigKey): unknown {\n return DEFAULTS[key];\n}\n\n/** Metadata for every known key. */\nexport function keyMeta(key: ConfigKey): KeyMeta {\n const d = KEY_DESCRIPTORS[key];\n return {\n key,\n tier: isBootstrapKey(key) ? \"bootstrap\" : \"runtime\",\n secret: isSecretKey(key),\n env: ENV_VARS[key],\n label: d.label,\n description: d.description,\n group: d.group,\n unit: d.unit,\n default: defaultForKey(key),\n editable: d.editable,\n };\n}\n\n/** All keys with metadata. */\nexport function allKeyMeta(): KeyMeta[] {\n return [...BOOTSTRAP_KEYS, ...RUNTIME_KEYS].map((k) => keyMeta(k));\n}\n\n/** The per-key zod schema (for validating a single runtime write). */\nexport function schemaForKey(key: ConfigKey): z.ZodTypeAny {\n return (\n (configSchema.shape as Record<string, z.ZodTypeAny>)[key] ?? z.unknown()\n );\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  export { AdminTokenMissingError, adminAuthMiddleware, adminEnabled, ensureAdminStorage, mountAdmin, readAdminToken, } from "./admin.js";
2
2
  export { analyticsEnabled, type InstallAnalyticsOptions, installAnalytics, mockEnabled, } from "./analytics.js";
3
3
  export { type AuthInfo, type AuthMetadataOptions, type BearerAuthMiddlewareOptions, InvalidTokenError, mcpAuthMetadataRouter, optionalBearerAuth, requireBearerAuth, } from "./auth.js";
4
- export { allKeyMeta, BOOTSTRAP_KEYS, type BootstrapConfig, type BootstrapKey, bootstrapSchema, type Config, type ConfigKey, type ConfigSource, configSchema, createConfigRouter, ENV_VARS, isBootstrapKey, isKnownKey, isRuntimeKey, isSecretKey, type KeyMeta, keyMeta, loadConfigFile, MASKED, type ResolvedConfig, type ResolvedSetting, RUNTIME_KEYS, type RuntimeConfig, type RuntimeKey, resolveConfig, runtimeSchema, SECRET_KEYS, validateRuntimeWrite, } from "./config/index.js";
4
+ export { allKeyMeta, BOOTSTRAP_KEYS, type BootstrapConfig, type BootstrapKey, bootstrapSchema, type Config, type ConfigKey, type ConfigSource, configSchema, createConfigRouter, defaultForKey, type Editable, ENV_VARS, editableOf, getPreset, isBootstrapKey, isKnownKey, isRestartKey, isRuntimeKey, isSecretKey, type KeyMeta, keyMeta, loadConfigFile, MASKED, PRESET_NAMES, PRESETS, type Preset, RESTART_KEYS, type ResolvedConfig, type ResolvedSetting, RUNTIME_KEYS, type RuntimeConfig, type RuntimeKey, resolveConfig, runtimeSchema, SECRET_KEYS, validateConfigWrite, validateRuntimeWrite, } from "./config/index.js";
5
5
  export { audio, embeddedResource, image, resourceLink, text, } from "./content-helpers.js";
6
6
  export { FileRef } from "./file-ref.js";
7
7
  export type { AnyToolRegistry, InferTools, ToolInput, ToolNames, ToolOutput, ToolResponseMetadata, } from "./inferUtilityTypes.js";
@@ -1,7 +1,7 @@
1
1
  export { AdminTokenMissingError, adminAuthMiddleware, adminEnabled, ensureAdminStorage, mountAdmin, readAdminToken, } from "./admin.js";
2
2
  export { analyticsEnabled, installAnalytics, mockEnabled, } from "./analytics.js";
3
3
  export { InvalidTokenError, mcpAuthMetadataRouter, optionalBearerAuth, requireBearerAuth, } from "./auth.js";
4
- export { allKeyMeta, BOOTSTRAP_KEYS, bootstrapSchema, configSchema, createConfigRouter, ENV_VARS, isBootstrapKey, isKnownKey, isRuntimeKey, isSecretKey, keyMeta, loadConfigFile, MASKED, RUNTIME_KEYS, resolveConfig, runtimeSchema, SECRET_KEYS, validateRuntimeWrite, } from "./config/index.js";
4
+ export { allKeyMeta, BOOTSTRAP_KEYS, bootstrapSchema, configSchema, createConfigRouter, defaultForKey, ENV_VARS, editableOf, getPreset, isBootstrapKey, isKnownKey, isRestartKey, isRuntimeKey, isSecretKey, keyMeta, loadConfigFile, MASKED, PRESET_NAMES, PRESETS, RESTART_KEYS, RUNTIME_KEYS, resolveConfig, runtimeSchema, SECRET_KEYS, validateConfigWrite, validateRuntimeWrite, } from "./config/index.js";
5
5
  export { audio, embeddedResource, image, resourceLink, text, } from "./content-helpers.js";
6
6
  export { FileRef } from "./file-ref.js";
7
7
  export { getActiveStorage, serverLog } from "./log-sink.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAIL,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,UAAU,EACV,cAAc,EAGd,eAAe,EAIf,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,OAAO,EACP,cAAc,EACd,MAAM,EAGN,YAAY,EAGZ,aAAa,EACb,aAAa,EACb,WAAW,EACX,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,IAAI,GACL,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AASxC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAU5D,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EAET,UAAU,EACV,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EAKzB,UAAU,EAEV,SAAS,GAGV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,QAAQ,EAER,WAAW,EACX,YAAY,GACb,MAAM,WAAW,CAAC;AAcnB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EAEpB,sBAAsB,EAEtB,sBAAsB,EACtB,+BAA+B,EAC/B,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAW5B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export {\n AdminTokenMissingError,\n adminAuthMiddleware,\n adminEnabled,\n ensureAdminStorage,\n mountAdmin,\n readAdminToken,\n} from \"./admin.js\";\nexport {\n analyticsEnabled,\n type InstallAnalyticsOptions,\n installAnalytics,\n mockEnabled,\n} from \"./analytics.js\";\nexport {\n type AuthInfo,\n type AuthMetadataOptions,\n type BearerAuthMiddlewareOptions,\n InvalidTokenError,\n mcpAuthMetadataRouter,\n optionalBearerAuth,\n requireBearerAuth,\n} from \"./auth.js\";\nexport {\n allKeyMeta,\n BOOTSTRAP_KEYS,\n type BootstrapConfig,\n type BootstrapKey,\n bootstrapSchema,\n type Config,\n type ConfigKey,\n type ConfigSource,\n configSchema,\n createConfigRouter,\n ENV_VARS,\n isBootstrapKey,\n isKnownKey,\n isRuntimeKey,\n isSecretKey,\n type KeyMeta,\n keyMeta,\n loadConfigFile,\n MASKED,\n type ResolvedConfig,\n type ResolvedSetting,\n RUNTIME_KEYS,\n type RuntimeConfig,\n type RuntimeKey,\n resolveConfig,\n runtimeSchema,\n SECRET_KEYS,\n validateRuntimeWrite,\n} from \"./config/index.js\";\nexport {\n audio,\n embeddedResource,\n image,\n resourceLink,\n text,\n} from \"./content-helpers.js\";\nexport { FileRef } from \"./file-ref.js\";\nexport type {\n AnyToolRegistry,\n InferTools,\n ToolInput,\n ToolNames,\n ToolOutput,\n ToolResponseMetadata,\n} from \"./inferUtilityTypes.js\";\nexport { getActiveStorage, serverLog } from \"./log-sink.js\";\nexport type {\n McpExtra,\n McpMethodString,\n McpMiddlewareFilter,\n McpMiddlewareFn,\n McpResultFor,\n McpTypedMiddlewareFn,\n McpWildcard,\n} from \"./middleware.js\";\nexport {\n generateMockEvents,\n generateMockLogs,\n MOCK_SEED,\n type MockSeedOptions,\n mulberry32,\n seedMockData,\n} from \"./mock-seed.js\";\nexport {\n createObservabilityRouter,\n type LatencyBucket,\n type MethodStat,\n type ObservabilityDisabled,\n type ObservabilitySummary,\n percentile,\n type SummarizeOptions,\n summarize,\n type TimeBucket,\n type ToolStat,\n} from \"./observability.js\";\nexport {\n createOtelSink,\n initOtel,\n type OtelSink,\n otelEnabled,\n otelEndpoint,\n} from \"./otel.js\";\nexport type {\n HandlerContent,\n KnownToolMeta,\n McpServerTypes,\n SecurityScheme,\n ToolDef,\n ToolMeta,\n ViewConfig,\n ViewCsp,\n ViewHostType,\n ViewName,\n ViewNameRegistry,\n} from \"./server.js\";\nexport {\n __setBuildManifest,\n McpServer,\n normalizeContent,\n} from \"./server.js\";\nexport {\n DEFAULT_DB_PATH,\n DEFAULT_MEMORY_CAP,\n MemoryStorageAdapter,\n type PgPoolLike,\n PostgresStorageAdapter,\n type PostgresStorageOptions,\n registerStorageAdapter,\n resolvePostgresConnectionString,\n resolveStorageAdapter,\n SqliteStorageAdapter,\n} from \"./storage/index.js\";\nexport type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterFactory,\n StorageAdapterOptions,\n} from \"./storage/types.js\";\nexport { viewsDevServer } from \"./viewsDevServer.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAIL,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,UAAU,EACV,cAAc,EAGd,eAAe,EAIf,YAAY,EACZ,kBAAkB,EAClB,aAAa,EAEb,QAAQ,EACR,UAAU,EACV,SAAS,EACT,cAAc,EACd,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EAEX,OAAO,EACP,cAAc,EACd,MAAM,EACN,YAAY,EACZ,OAAO,EAEP,YAAY,EAGZ,YAAY,EAGZ,aAAa,EACb,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,IAAI,GACL,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AASxC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAU5D,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EAET,UAAU,EACV,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EAKzB,UAAU,EAEV,SAAS,GAGV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,QAAQ,EAER,WAAW,EACX,YAAY,GACb,MAAM,WAAW,CAAC;AAcnB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EAEpB,sBAAsB,EAEtB,sBAAsB,EACtB,+BAA+B,EAC/B,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAW5B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export {\n AdminTokenMissingError,\n adminAuthMiddleware,\n adminEnabled,\n ensureAdminStorage,\n mountAdmin,\n readAdminToken,\n} from \"./admin.js\";\nexport {\n analyticsEnabled,\n type InstallAnalyticsOptions,\n installAnalytics,\n mockEnabled,\n} from \"./analytics.js\";\nexport {\n type AuthInfo,\n type AuthMetadataOptions,\n type BearerAuthMiddlewareOptions,\n InvalidTokenError,\n mcpAuthMetadataRouter,\n optionalBearerAuth,\n requireBearerAuth,\n} from \"./auth.js\";\nexport {\n allKeyMeta,\n BOOTSTRAP_KEYS,\n type BootstrapConfig,\n type BootstrapKey,\n bootstrapSchema,\n type Config,\n type ConfigKey,\n type ConfigSource,\n configSchema,\n createConfigRouter,\n defaultForKey,\n type Editable,\n ENV_VARS,\n editableOf,\n getPreset,\n isBootstrapKey,\n isKnownKey,\n isRestartKey,\n isRuntimeKey,\n isSecretKey,\n type KeyMeta,\n keyMeta,\n loadConfigFile,\n MASKED,\n PRESET_NAMES,\n PRESETS,\n type Preset,\n RESTART_KEYS,\n type ResolvedConfig,\n type ResolvedSetting,\n RUNTIME_KEYS,\n type RuntimeConfig,\n type RuntimeKey,\n resolveConfig,\n runtimeSchema,\n SECRET_KEYS,\n validateConfigWrite,\n validateRuntimeWrite,\n} from \"./config/index.js\";\nexport {\n audio,\n embeddedResource,\n image,\n resourceLink,\n text,\n} from \"./content-helpers.js\";\nexport { FileRef } from \"./file-ref.js\";\nexport type {\n AnyToolRegistry,\n InferTools,\n ToolInput,\n ToolNames,\n ToolOutput,\n ToolResponseMetadata,\n} from \"./inferUtilityTypes.js\";\nexport { getActiveStorage, serverLog } from \"./log-sink.js\";\nexport type {\n McpExtra,\n McpMethodString,\n McpMiddlewareFilter,\n McpMiddlewareFn,\n McpResultFor,\n McpTypedMiddlewareFn,\n McpWildcard,\n} from \"./middleware.js\";\nexport {\n generateMockEvents,\n generateMockLogs,\n MOCK_SEED,\n type MockSeedOptions,\n mulberry32,\n seedMockData,\n} from \"./mock-seed.js\";\nexport {\n createObservabilityRouter,\n type LatencyBucket,\n type MethodStat,\n type ObservabilityDisabled,\n type ObservabilitySummary,\n percentile,\n type SummarizeOptions,\n summarize,\n type TimeBucket,\n type ToolStat,\n} from \"./observability.js\";\nexport {\n createOtelSink,\n initOtel,\n type OtelSink,\n otelEnabled,\n otelEndpoint,\n} from \"./otel.js\";\nexport type {\n HandlerContent,\n KnownToolMeta,\n McpServerTypes,\n SecurityScheme,\n ToolDef,\n ToolMeta,\n ViewConfig,\n ViewCsp,\n ViewHostType,\n ViewName,\n ViewNameRegistry,\n} from \"./server.js\";\nexport {\n __setBuildManifest,\n McpServer,\n normalizeContent,\n} from \"./server.js\";\nexport {\n DEFAULT_DB_PATH,\n DEFAULT_MEMORY_CAP,\n MemoryStorageAdapter,\n type PgPoolLike,\n PostgresStorageAdapter,\n type PostgresStorageOptions,\n registerStorageAdapter,\n resolvePostgresConnectionString,\n resolveStorageAdapter,\n SqliteStorageAdapter,\n} from \"./storage/index.js\";\nexport type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterFactory,\n StorageAdapterOptions,\n} from \"./storage/types.js\";\nexport { viewsDevServer } from \"./viewsDevServer.js\";\n"]}
@@ -22,6 +22,7 @@ export declare class MemoryStorageAdapter implements StorageAdapter {
22
22
  queryLogs(f?: LogQuery): Promise<LogEntry[]>;
23
23
  getConfig(key: string): Promise<unknown>;
24
24
  setConfig(key: string, value: unknown, actor?: string): Promise<void>;
25
+ clearConfig(key: string, actor?: string): Promise<void>;
25
26
  allConfig(): Promise<Record<string, unknown>>;
26
27
  getConfigAudit(): Promise<ConfigAuditEntry[]>;
27
28
  close(): Promise<void>;
@@ -73,6 +73,20 @@ export class MemoryStorageAdapter {
73
73
  actor: actor ?? "system",
74
74
  });
75
75
  }
76
+ async clearConfig(key, actor) {
77
+ if (!this.config.has(key)) {
78
+ return;
79
+ }
80
+ const oldValue = this.config.get(key);
81
+ this.config.delete(key);
82
+ this.audit.push({
83
+ ts: Date.now(),
84
+ key,
85
+ oldValue,
86
+ newValue: undefined,
87
+ actor: actor ?? "system",
88
+ });
89
+ }
76
90
  async allConfig() {
77
91
  return Object.fromEntries(this.config);
78
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../../src/server/storage/memory.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterOptions,\n} from \"./types.js\";\n\n/** Default ring-buffer capacity for events and logs. */\nexport const DEFAULT_MEMORY_CAP = 5000;\n\n/**\n * In-memory {@link StorageAdapter}. Zero dependencies; the dev default.\n *\n * Events and logs live in fixed-capacity ring buffers (oldest dropped first);\n * config lives in a `Map`; config changes append to an in-memory audit list.\n * Nothing persists across process restarts.\n */\nexport class MemoryStorageAdapter implements StorageAdapter {\n private readonly cap: number;\n private readonly events: AnalyticsEvent[] = [];\n private readonly logs: LogEntry[] = [];\n private readonly config = new Map<string, unknown>();\n private readonly audit: ConfigAuditEntry[] = [];\n\n constructor(opts?: StorageAdapterOptions) {\n const cap = opts?.cap ?? DEFAULT_MEMORY_CAP;\n this.cap = cap > 0 ? cap : DEFAULT_MEMORY_CAP;\n }\n\n async init(): Promise<void> {\n // No setup required.\n }\n\n async recordEvent(e: AnalyticsEvent): Promise<void> {\n push(this.events, e, this.cap);\n }\n\n async queryEvents(f: EventQuery = {}): Promise<AnalyticsEvent[]> {\n let out = this.events;\n if (f.since !== undefined) {\n out = out.filter((e) => e.ts >= (f.since as number));\n }\n if (f.type !== undefined) {\n out = out.filter((e) => e.type === f.type);\n }\n if (f.tool !== undefined) {\n out = out.filter((e) => e.tool === f.tool);\n }\n // Most recent first.\n out = out.slice().reverse();\n if (f.limit !== undefined && f.limit >= 0) {\n out = out.slice(0, f.limit);\n }\n // Defensive copy so callers cannot mutate the buffer.\n return out.map((e) => ({ ...e }));\n }\n\n async appendLog(l: LogEntry): Promise<void> {\n push(this.logs, l, this.cap);\n }\n\n async queryLogs(f: LogQuery = {}): Promise<LogEntry[]> {\n let out = this.logs;\n if (f.since !== undefined) {\n out = out.filter((l) => l.ts >= (f.since as number));\n }\n if (f.level !== undefined) {\n out = out.filter((l) => l.level === f.level);\n }\n out = out.slice().reverse();\n if (f.limit !== undefined && f.limit >= 0) {\n out = out.slice(0, f.limit);\n }\n return out.map((l) => ({ ...l }));\n }\n\n async getConfig(key: string): Promise<unknown> {\n return this.config.get(key);\n }\n\n async setConfig(key: string, value: unknown, actor?: string): Promise<void> {\n const oldValue = this.config.get(key);\n this.config.set(key, value);\n this.audit.push({\n ts: Date.now(),\n key,\n oldValue,\n newValue: value,\n actor: actor ?? \"system\",\n });\n }\n\n async allConfig(): Promise<Record<string, unknown>> {\n return Object.fromEntries(this.config);\n }\n\n async getConfigAudit(): Promise<ConfigAuditEntry[]> {\n // Stored oldest-first; return most-recent-first to match the interface.\n return this.audit.map((a) => ({ ...a })).reverse();\n }\n\n async close(): Promise<void> {\n // Nothing to release.\n }\n\n /** Audit trail of config writes (most recent last). Synchronous helper for tests. */\n getAuditLog(): ConfigAuditEntry[] {\n return this.audit.map((a) => ({ ...a }));\n }\n}\n\n/** Append to a ring buffer, dropping the oldest entries past `cap`. */\nfunction push<T>(buf: T[], item: T, cap: number): void {\n buf.push(item);\n if (buf.length > cap) {\n buf.splice(0, buf.length - cap);\n }\n}\n"]}
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../../src/server/storage/memory.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterOptions,\n} from \"./types.js\";\n\n/** Default ring-buffer capacity for events and logs. */\nexport const DEFAULT_MEMORY_CAP = 5000;\n\n/**\n * In-memory {@link StorageAdapter}. Zero dependencies; the dev default.\n *\n * Events and logs live in fixed-capacity ring buffers (oldest dropped first);\n * config lives in a `Map`; config changes append to an in-memory audit list.\n * Nothing persists across process restarts.\n */\nexport class MemoryStorageAdapter implements StorageAdapter {\n private readonly cap: number;\n private readonly events: AnalyticsEvent[] = [];\n private readonly logs: LogEntry[] = [];\n private readonly config = new Map<string, unknown>();\n private readonly audit: ConfigAuditEntry[] = [];\n\n constructor(opts?: StorageAdapterOptions) {\n const cap = opts?.cap ?? DEFAULT_MEMORY_CAP;\n this.cap = cap > 0 ? cap : DEFAULT_MEMORY_CAP;\n }\n\n async init(): Promise<void> {\n // No setup required.\n }\n\n async recordEvent(e: AnalyticsEvent): Promise<void> {\n push(this.events, e, this.cap);\n }\n\n async queryEvents(f: EventQuery = {}): Promise<AnalyticsEvent[]> {\n let out = this.events;\n if (f.since !== undefined) {\n out = out.filter((e) => e.ts >= (f.since as number));\n }\n if (f.type !== undefined) {\n out = out.filter((e) => e.type === f.type);\n }\n if (f.tool !== undefined) {\n out = out.filter((e) => e.tool === f.tool);\n }\n // Most recent first.\n out = out.slice().reverse();\n if (f.limit !== undefined && f.limit >= 0) {\n out = out.slice(0, f.limit);\n }\n // Defensive copy so callers cannot mutate the buffer.\n return out.map((e) => ({ ...e }));\n }\n\n async appendLog(l: LogEntry): Promise<void> {\n push(this.logs, l, this.cap);\n }\n\n async queryLogs(f: LogQuery = {}): Promise<LogEntry[]> {\n let out = this.logs;\n if (f.since !== undefined) {\n out = out.filter((l) => l.ts >= (f.since as number));\n }\n if (f.level !== undefined) {\n out = out.filter((l) => l.level === f.level);\n }\n out = out.slice().reverse();\n if (f.limit !== undefined && f.limit >= 0) {\n out = out.slice(0, f.limit);\n }\n return out.map((l) => ({ ...l }));\n }\n\n async getConfig(key: string): Promise<unknown> {\n return this.config.get(key);\n }\n\n async setConfig(key: string, value: unknown, actor?: string): Promise<void> {\n const oldValue = this.config.get(key);\n this.config.set(key, value);\n this.audit.push({\n ts: Date.now(),\n key,\n oldValue,\n newValue: value,\n actor: actor ?? \"system\",\n });\n }\n\n async clearConfig(key: string, actor?: string): Promise<void> {\n if (!this.config.has(key)) {\n return;\n }\n const oldValue = this.config.get(key);\n this.config.delete(key);\n this.audit.push({\n ts: Date.now(),\n key,\n oldValue,\n newValue: undefined,\n actor: actor ?? \"system\",\n });\n }\n\n async allConfig(): Promise<Record<string, unknown>> {\n return Object.fromEntries(this.config);\n }\n\n async getConfigAudit(): Promise<ConfigAuditEntry[]> {\n // Stored oldest-first; return most-recent-first to match the interface.\n return this.audit.map((a) => ({ ...a })).reverse();\n }\n\n async close(): Promise<void> {\n // Nothing to release.\n }\n\n /** Audit trail of config writes (most recent last). Synchronous helper for tests. */\n getAuditLog(): ConfigAuditEntry[] {\n return this.audit.map((a) => ({ ...a }));\n }\n}\n\n/** Append to a ring buffer, dropping the oldest entries past `cap`. */\nfunction push<T>(buf: T[], item: T, cap: number): void {\n buf.push(item);\n if (buf.length > cap) {\n buf.splice(0, buf.length - cap);\n }\n}\n"]}
@@ -76,6 +76,23 @@ describe("MemoryStorageAdapter", () => {
76
76
  actor: "alice",
77
77
  });
78
78
  });
79
+ it("clearConfig removes the override and audits old → undefined", async () => {
80
+ await store.setConfig("k", "v1");
81
+ await store.clearConfig("k", "bob");
82
+ expect(await store.getConfig("k")).toBeUndefined();
83
+ expect(await store.allConfig()).toEqual({});
84
+ const audit = await store.getConfigAudit();
85
+ expect(audit[0]).toMatchObject({
86
+ key: "k",
87
+ oldValue: "v1",
88
+ newValue: undefined,
89
+ actor: "bob",
90
+ });
91
+ });
92
+ it("clearConfig is a no-op (no audit) when the key was never set", async () => {
93
+ await store.clearConfig("absent");
94
+ expect(await store.getConfigAudit()).toHaveLength(0);
95
+ });
79
96
  });
80
97
  });
81
98
  //# sourceMappingURL=memory.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.test.js","sourceRoot":"","sources":["../../../src/server/storage/memory.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { beforeEach, describe, expect, it } from \"vitest\";\nimport { MemoryStorageAdapter } from \"./memory.js\";\n\ndescribe(\"MemoryStorageAdapter\", () => {\n let store: MemoryStorageAdapter;\n\n beforeEach(async () => {\n store = new MemoryStorageAdapter({ cap: 100 });\n await store.init();\n });\n\n describe(\"events\", () => {\n it(\"records and queries events, most recent first\", async () => {\n await store.recordEvent({ ts: 1, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 2, type: \"tool_call\", tool: \"b\" });\n const all = await store.queryEvents({});\n expect(all.map((e) => e.tool)).toEqual([\"b\", \"a\"]);\n });\n\n it(\"filters by since, tool, and limit\", async () => {\n await store.recordEvent({ ts: 1, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 5, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 10, type: \"ping\", tool: \"b\" });\n\n expect(await store.queryEvents({ since: 5 })).toHaveLength(2);\n expect(await store.queryEvents({ tool: \"a\" })).toHaveLength(2);\n expect(await store.queryEvents({ type: \"ping\" })).toHaveLength(1);\n expect(await store.queryEvents({ limit: 1 })).toHaveLength(1);\n expect((await store.queryEvents({ limit: 1 }))[0]?.ts).toBe(10);\n });\n\n it(\"drops oldest events past the cap\", async () => {\n const small = new MemoryStorageAdapter({ cap: 3 });\n await small.init();\n for (let i = 0; i < 10; i++) {\n await small.recordEvent({ ts: i, type: \"tool_call\" });\n }\n const all = await small.queryEvents({});\n expect(all).toHaveLength(3);\n expect(all.map((e) => e.ts)).toEqual([9, 8, 7]);\n });\n });\n\n describe(\"logs\", () => {\n it(\"appends and queries logs with level + limit filters\", async () => {\n await store.appendLog({ ts: 1, level: \"info\", msg: \"one\" });\n await store.appendLog({ ts: 2, level: \"error\", msg: \"two\" });\n await store.appendLog({ ts: 3, level: \"info\", msg: \"three\" });\n\n const all = await store.queryLogs({});\n expect(all.map((l) => l.msg)).toEqual([\"three\", \"two\", \"one\"]);\n expect(await store.queryLogs({ level: \"error\" })).toHaveLength(1);\n expect(await store.queryLogs({ since: 2 })).toHaveLength(2);\n expect(await store.queryLogs({ limit: 1 })).toHaveLength(1);\n });\n });\n\n describe(\"config + audit\", () => {\n it(\"get/set/all round-trips opaque values\", async () => {\n expect(await store.getConfig(\"missing\")).toBeUndefined();\n await store.setConfig(\"analytics.enabled\", true);\n await store.setConfig(\"retention\", { days: 7 });\n expect(await store.getConfig(\"analytics.enabled\")).toBe(true);\n expect(await store.getConfig(\"retention\")).toEqual({ days: 7 });\n expect(await store.allConfig()).toEqual({\n \"analytics.enabled\": true,\n retention: { days: 7 },\n });\n });\n\n it(\"writes an audit row on setConfig with old → new + actor\", async () => {\n await store.setConfig(\"k\", \"v1\");\n await store.setConfig(\"k\", \"v2\", \"alice\");\n const audit = store.getAuditLog();\n expect(audit).toHaveLength(2);\n expect(audit[0]).toMatchObject({\n key: \"k\",\n oldValue: undefined,\n newValue: \"v1\",\n actor: \"system\",\n });\n expect(audit[1]).toMatchObject({\n key: \"k\",\n oldValue: \"v1\",\n newValue: \"v2\",\n actor: \"alice\",\n });\n });\n });\n});\n"]}
1
+ {"version":3,"file":"memory.test.js","sourceRoot":"","sources":["../../../src/server/storage/memory.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { beforeEach, describe, expect, it } from \"vitest\";\nimport { MemoryStorageAdapter } from \"./memory.js\";\n\ndescribe(\"MemoryStorageAdapter\", () => {\n let store: MemoryStorageAdapter;\n\n beforeEach(async () => {\n store = new MemoryStorageAdapter({ cap: 100 });\n await store.init();\n });\n\n describe(\"events\", () => {\n it(\"records and queries events, most recent first\", async () => {\n await store.recordEvent({ ts: 1, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 2, type: \"tool_call\", tool: \"b\" });\n const all = await store.queryEvents({});\n expect(all.map((e) => e.tool)).toEqual([\"b\", \"a\"]);\n });\n\n it(\"filters by since, tool, and limit\", async () => {\n await store.recordEvent({ ts: 1, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 5, type: \"tool_call\", tool: \"a\" });\n await store.recordEvent({ ts: 10, type: \"ping\", tool: \"b\" });\n\n expect(await store.queryEvents({ since: 5 })).toHaveLength(2);\n expect(await store.queryEvents({ tool: \"a\" })).toHaveLength(2);\n expect(await store.queryEvents({ type: \"ping\" })).toHaveLength(1);\n expect(await store.queryEvents({ limit: 1 })).toHaveLength(1);\n expect((await store.queryEvents({ limit: 1 }))[0]?.ts).toBe(10);\n });\n\n it(\"drops oldest events past the cap\", async () => {\n const small = new MemoryStorageAdapter({ cap: 3 });\n await small.init();\n for (let i = 0; i < 10; i++) {\n await small.recordEvent({ ts: i, type: \"tool_call\" });\n }\n const all = await small.queryEvents({});\n expect(all).toHaveLength(3);\n expect(all.map((e) => e.ts)).toEqual([9, 8, 7]);\n });\n });\n\n describe(\"logs\", () => {\n it(\"appends and queries logs with level + limit filters\", async () => {\n await store.appendLog({ ts: 1, level: \"info\", msg: \"one\" });\n await store.appendLog({ ts: 2, level: \"error\", msg: \"two\" });\n await store.appendLog({ ts: 3, level: \"info\", msg: \"three\" });\n\n const all = await store.queryLogs({});\n expect(all.map((l) => l.msg)).toEqual([\"three\", \"two\", \"one\"]);\n expect(await store.queryLogs({ level: \"error\" })).toHaveLength(1);\n expect(await store.queryLogs({ since: 2 })).toHaveLength(2);\n expect(await store.queryLogs({ limit: 1 })).toHaveLength(1);\n });\n });\n\n describe(\"config + audit\", () => {\n it(\"get/set/all round-trips opaque values\", async () => {\n expect(await store.getConfig(\"missing\")).toBeUndefined();\n await store.setConfig(\"analytics.enabled\", true);\n await store.setConfig(\"retention\", { days: 7 });\n expect(await store.getConfig(\"analytics.enabled\")).toBe(true);\n expect(await store.getConfig(\"retention\")).toEqual({ days: 7 });\n expect(await store.allConfig()).toEqual({\n \"analytics.enabled\": true,\n retention: { days: 7 },\n });\n });\n\n it(\"writes an audit row on setConfig with old → new + actor\", async () => {\n await store.setConfig(\"k\", \"v1\");\n await store.setConfig(\"k\", \"v2\", \"alice\");\n const audit = store.getAuditLog();\n expect(audit).toHaveLength(2);\n expect(audit[0]).toMatchObject({\n key: \"k\",\n oldValue: undefined,\n newValue: \"v1\",\n actor: \"system\",\n });\n expect(audit[1]).toMatchObject({\n key: \"k\",\n oldValue: \"v1\",\n newValue: \"v2\",\n actor: \"alice\",\n });\n });\n\n it(\"clearConfig removes the override and audits old → undefined\", async () => {\n await store.setConfig(\"k\", \"v1\");\n await store.clearConfig(\"k\", \"bob\");\n expect(await store.getConfig(\"k\")).toBeUndefined();\n expect(await store.allConfig()).toEqual({});\n const audit = await store.getConfigAudit();\n expect(audit[0]).toMatchObject({\n key: \"k\",\n oldValue: \"v1\",\n newValue: undefined,\n actor: \"bob\",\n });\n });\n\n it(\"clearConfig is a no-op (no audit) when the key was never set\", async () => {\n await store.clearConfig(\"absent\");\n expect(await store.getConfigAudit()).toHaveLength(0);\n });\n });\n});\n"]}
@@ -58,6 +58,7 @@ export declare class PostgresStorageAdapter implements StorageAdapter {
58
58
  queryLogs(f?: LogQuery): Promise<LogEntry[]>;
59
59
  getConfig(key: string): Promise<unknown>;
60
60
  setConfig(key: string, value: unknown, actor?: string): Promise<void>;
61
+ clearConfig(key: string, actor?: string): Promise<void>;
61
62
  allConfig(): Promise<Record<string, unknown>>;
62
63
  getConfigAudit(): Promise<ConfigAuditEntry[]>;
63
64
  close(): Promise<void>;
@@ -126,6 +126,18 @@ export class PostgresStorageAdapter {
126
126
  await pool.query("INSERT INTO config (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", [key, serialized]);
127
127
  await pool.query("INSERT INTO config_audit (ts, key, old_value, new_value, actor) VALUES ($1, $2, $3, $4, $5)", [Date.now(), key, oldSerialized, serialized, actor ?? "system"]);
128
128
  }
129
+ async clearConfig(key, actor) {
130
+ const pool = this.require();
131
+ const { rows } = await pool.query("SELECT value FROM config WHERE key = $1", [key]);
132
+ const existing = rows[0];
133
+ if (existing === undefined) {
134
+ return;
135
+ }
136
+ await pool.query("DELETE FROM config WHERE key = $1", [key]);
137
+ // `new_value` is NOT NULL; a reset stores JSON `null` to represent
138
+ // "reset to default" (getConfigAudit parses it back to `null`).
139
+ await pool.query("INSERT INTO config_audit (ts, key, old_value, new_value, actor) VALUES ($1, $2, $3, $4, $5)", [Date.now(), key, existing.value, "null", actor ?? "system"]);
140
+ }
129
141
  async allConfig() {
130
142
  const pool = this.require();
131
143
  const { rows } = await pool.query("SELECT key, value FROM config");
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/server/storage/postgres.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterOptions,\n} from \"./types.js\";\n\n/**\n * PostgreSQL {@link StorageAdapter}, backed by `pg` (node-postgres).\n *\n * `pg` is pure-JS (no native build), so it installs cleanly in CI on both\n * macOS-arm64 and linux-x64. The connection is configured ENTIRELY from the\n * environment — never hardcoded:\n *\n * - `ENPILINK_DB_URL` (enpilink-specific) takes precedence, else\n * - `DATABASE_URL` (the de-facto standard), else\n * - the standard `PG*` vars (`PGHOST`/`PGPORT`/`PGUSER`/`PGPASSWORD`/`PGDATABASE`)\n * which `pg` reads on its own when no `connectionString` is given.\n *\n * Tables `events`, `logs`, `config`, `config_audit` are created-if-not-exists in\n * {@link init}. Config values are stored as opaque JSON text — no special-casing\n * of keys. Writes use prepared/parameterized statements; `recordEvent` /\n * `appendLog` are single cheap inserts so the ~600-write mock seed stays fast.\n */\n\n/** Minimal structural type for a `pg` query result (avoids a hard type dep). */\ninterface PgQueryResult<R = Record<string, unknown>> {\n rows: R[];\n}\n\n/** Minimal structural type for a `pg` Pool — enough for this adapter. */\nexport interface PgPoolLike {\n query<R = Record<string, unknown>>(\n text: string,\n values?: unknown[],\n ): Promise<PgQueryResult<R>>;\n end(): Promise<void>;\n}\n\n/** Options specific to the postgres adapter. */\nexport interface PostgresStorageOptions extends StorageAdapterOptions {\n /**\n * Inject a `pg`-compatible Pool (used by tests with `pg-mem`). When omitted,\n * a real `pg.Pool` is constructed lazily in {@link init} from the environment.\n */\n pool?: PgPoolLike;\n /**\n * Explicit connection string. Normally left undefined so the connection is\n * resolved from `ENPILINK_DB_URL` / `DATABASE_URL` / `PG*` env vars. Never\n * hardcode a connection string in source.\n */\n connectionString?: string;\n}\n\n/**\n * Resolve the connection string from the environment. Returns `undefined` when\n * none of the enpilink/standard URL vars are set — in that case `pg` falls back\n * to its own `PG*` env-var handling. NEVER returns a hardcoded default.\n */\nexport function resolvePostgresConnectionString(): string | undefined {\n // Treat blank/whitespace-only values as unset so a blank var never shadows a\n // real one (and never becomes a hardcoded-looking default).\n const enpilink = process.env.ENPILINK_DB_URL?.trim();\n if (enpilink) {\n return enpilink;\n }\n const standard = process.env.DATABASE_URL?.trim();\n return standard ? standard : undefined;\n}\n\nexport class PostgresStorageAdapter implements StorageAdapter {\n private readonly injectedPool?: PgPoolLike;\n private readonly connectionString?: string;\n private pool: PgPoolLike | null = null;\n\n constructor(opts?: PostgresStorageOptions) {\n this.injectedPool = opts?.pool;\n this.connectionString =\n opts?.connectionString ?? resolvePostgresConnectionString();\n }\n\n async init(): Promise<void> {\n if (this.pool) {\n return;\n }\n if (this.injectedPool) {\n this.pool = this.injectedPool;\n } else {\n // Dynamic import keeps `pg` out of the load path until a postgres adapter\n // is actually instantiated (memory/sqlite users pay nothing).\n const mod = await import(\"pg\");\n const Pool = (mod.default?.Pool ??\n (mod as unknown as { Pool: new (cfg?: unknown) => PgPoolLike })\n .Pool) as new (\n cfg?: unknown,\n ) => PgPoolLike;\n // When no connectionString is resolved, pass none so `pg` reads PG* vars.\n this.pool = this.connectionString\n ? new Pool({ connectionString: this.connectionString })\n : new Pool();\n }\n await this.pool.query(SCHEMA);\n }\n\n private require(): PgPoolLike {\n if (!this.pool) {\n throw new Error(\"PostgresStorageAdapter: call init() before use\");\n }\n return this.pool;\n }\n\n async recordEvent(e: AnalyticsEvent): Promise<void> {\n const pool = this.require();\n await pool.query(\n \"INSERT INTO events (ts, type, tool, method, ms, ok, error, meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\",\n [\n e.ts,\n e.type,\n e.tool ?? null,\n e.method ?? null,\n e.ms ?? null,\n e.ok === undefined ? null : e.ok,\n e.error ?? null,\n e.meta === undefined ? null : JSON.stringify(e.meta),\n ],\n );\n }\n\n async queryEvents(f: EventQuery = {}): Promise<AnalyticsEvent[]> {\n const pool = this.require();\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.since !== undefined) {\n params.push(f.since);\n where.push(`ts >= $${params.length}`);\n }\n if (f.type !== undefined) {\n params.push(f.type);\n where.push(`type = $${params.length}`);\n }\n if (f.tool !== undefined) {\n params.push(f.tool);\n where.push(`tool = $${params.length}`);\n }\n const clause = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const limit = limitClause(f.limit, params);\n const { rows } = await pool.query<EventRow>(\n `SELECT ts, type, tool, method, ms, ok, error, meta FROM events ${clause} ORDER BY id DESC ${limit}`,\n params,\n );\n return rows.map(rowToEvent);\n }\n\n async appendLog(l: LogEntry): Promise<void> {\n const pool = this.require();\n await pool.query(\n \"INSERT INTO logs (ts, level, msg, data) VALUES ($1, $2, $3, $4)\",\n [\n l.ts,\n l.level,\n l.msg,\n l.data === undefined ? null : JSON.stringify(l.data),\n ],\n );\n }\n\n async queryLogs(f: LogQuery = {}): Promise<LogEntry[]> {\n const pool = this.require();\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.since !== undefined) {\n params.push(f.since);\n where.push(`ts >= $${params.length}`);\n }\n if (f.level !== undefined) {\n params.push(f.level);\n where.push(`level = $${params.length}`);\n }\n const clause = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const limit = limitClause(f.limit, params);\n const { rows } = await pool.query<LogRow>(\n `SELECT ts, level, msg, data FROM logs ${clause} ORDER BY id DESC ${limit}`,\n params,\n );\n return rows.map(rowToLog);\n }\n\n async getConfig(key: string): Promise<unknown> {\n const pool = this.require();\n const { rows } = await pool.query<{ value: string }>(\n \"SELECT value FROM config WHERE key = $1\",\n [key],\n );\n const row = rows[0];\n return row === undefined ? undefined : JSON.parse(row.value);\n }\n\n async setConfig(key: string, value: unknown, actor?: string): Promise<void> {\n const pool = this.require();\n const serialized = JSON.stringify(value);\n // Read-old → upsert-new → write-audit. pg-mem does not implement\n // transactions identically across versions, so we read-then-write without a\n // BEGIN/COMMIT wrapper; the audit row records the prior value either way.\n const { rows } = await pool.query<{ value: string }>(\n \"SELECT value FROM config WHERE key = $1\",\n [key],\n );\n const oldSerialized = rows[0]?.value ?? null;\n await pool.query(\n \"INSERT INTO config (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2\",\n [key, serialized],\n );\n await pool.query(\n \"INSERT INTO config_audit (ts, key, old_value, new_value, actor) VALUES ($1, $2, $3, $4, $5)\",\n [Date.now(), key, oldSerialized, serialized, actor ?? \"system\"],\n );\n }\n\n async allConfig(): Promise<Record<string, unknown>> {\n const pool = this.require();\n const { rows } = await pool.query<{ key: string; value: string }>(\n \"SELECT key, value FROM config\",\n );\n const out: Record<string, unknown> = {};\n for (const r of rows) {\n out[r.key] = JSON.parse(r.value);\n }\n return out;\n }\n\n async getConfigAudit(): Promise<ConfigAuditEntry[]> {\n const pool = this.require();\n const { rows } = await pool.query<AuditRow>(\n \"SELECT ts, key, old_value, new_value, actor FROM config_audit ORDER BY id DESC\",\n );\n return rows.map((r) => ({\n ts: Number(r.ts),\n key: r.key,\n oldValue: r.old_value === null ? undefined : JSON.parse(r.old_value),\n newValue: JSON.parse(r.new_value),\n actor: r.actor,\n }));\n }\n\n async close(): Promise<void> {\n if (this.pool) {\n await this.pool.end();\n this.pool = null;\n }\n }\n}\n\nfunction limitClause(limit: number | undefined, params: unknown[]): string {\n if (limit === undefined || limit < 0) {\n return \"\";\n }\n params.push(limit);\n return `LIMIT $${params.length}`;\n}\n\ninterface EventRow {\n ts: number | string;\n type: string;\n tool: string | null;\n method: string | null;\n ms: number | string | null;\n ok: boolean | null;\n error: string | null;\n meta: string | null;\n}\n\ninterface LogRow {\n ts: number | string;\n level: string;\n msg: string;\n data: string | null;\n}\n\ninterface AuditRow {\n ts: number | string;\n key: string;\n old_value: string | null;\n new_value: string;\n actor: string;\n}\n\nfunction rowToEvent(r: EventRow): AnalyticsEvent {\n const e: AnalyticsEvent = { ts: Number(r.ts), type: r.type };\n if (r.tool !== null) {\n e.tool = r.tool;\n }\n if (r.method !== null) {\n e.method = r.method;\n }\n if (r.ms !== null) {\n e.ms = Number(r.ms);\n }\n if (r.ok !== null) {\n e.ok = r.ok;\n }\n if (r.error !== null) {\n e.error = r.error;\n }\n if (r.meta !== null) {\n e.meta = JSON.parse(r.meta);\n }\n return e;\n}\n\nfunction rowToLog(r: LogRow): LogEntry {\n const l: LogEntry = {\n ts: Number(r.ts),\n level: r.level as LogEntry[\"level\"],\n msg: r.msg,\n };\n if (r.data !== null) {\n l.data = JSON.parse(r.data);\n }\n return l;\n}\n\n/**\n * Schema. `BIGINT` for epoch-ms timestamps; `BIGSERIAL` ids give a stable\n * insertion order for most-recent-first queries (`ORDER BY id DESC`). `meta` /\n * `data` / config values are opaque JSON text.\n */\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n type TEXT NOT NULL,\n tool TEXT,\n method TEXT,\n ms BIGINT,\n ok BOOLEAN,\n error TEXT,\n meta TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events (type);\nCREATE INDEX IF NOT EXISTS idx_events_tool ON events (tool);\n\nCREATE TABLE IF NOT EXISTS logs (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n level TEXT NOT NULL,\n msg TEXT NOT NULL,\n data TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_logs_ts ON logs (ts);\nCREATE INDEX IF NOT EXISTS idx_logs_level ON logs (level);\n\nCREATE TABLE IF NOT EXISTS config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS config_audit (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n key TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT NOT NULL,\n actor TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_audit_key ON config_audit (key);\n`;\n"]}
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/server/storage/postgres.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AnalyticsEvent,\n ConfigAuditEntry,\n EventQuery,\n LogEntry,\n LogQuery,\n StorageAdapter,\n StorageAdapterOptions,\n} from \"./types.js\";\n\n/**\n * PostgreSQL {@link StorageAdapter}, backed by `pg` (node-postgres).\n *\n * `pg` is pure-JS (no native build), so it installs cleanly in CI on both\n * macOS-arm64 and linux-x64. The connection is configured ENTIRELY from the\n * environment — never hardcoded:\n *\n * - `ENPILINK_DB_URL` (enpilink-specific) takes precedence, else\n * - `DATABASE_URL` (the de-facto standard), else\n * - the standard `PG*` vars (`PGHOST`/`PGPORT`/`PGUSER`/`PGPASSWORD`/`PGDATABASE`)\n * which `pg` reads on its own when no `connectionString` is given.\n *\n * Tables `events`, `logs`, `config`, `config_audit` are created-if-not-exists in\n * {@link init}. Config values are stored as opaque JSON text — no special-casing\n * of keys. Writes use prepared/parameterized statements; `recordEvent` /\n * `appendLog` are single cheap inserts so the ~600-write mock seed stays fast.\n */\n\n/** Minimal structural type for a `pg` query result (avoids a hard type dep). */\ninterface PgQueryResult<R = Record<string, unknown>> {\n rows: R[];\n}\n\n/** Minimal structural type for a `pg` Pool — enough for this adapter. */\nexport interface PgPoolLike {\n query<R = Record<string, unknown>>(\n text: string,\n values?: unknown[],\n ): Promise<PgQueryResult<R>>;\n end(): Promise<void>;\n}\n\n/** Options specific to the postgres adapter. */\nexport interface PostgresStorageOptions extends StorageAdapterOptions {\n /**\n * Inject a `pg`-compatible Pool (used by tests with `pg-mem`). When omitted,\n * a real `pg.Pool` is constructed lazily in {@link init} from the environment.\n */\n pool?: PgPoolLike;\n /**\n * Explicit connection string. Normally left undefined so the connection is\n * resolved from `ENPILINK_DB_URL` / `DATABASE_URL` / `PG*` env vars. Never\n * hardcode a connection string in source.\n */\n connectionString?: string;\n}\n\n/**\n * Resolve the connection string from the environment. Returns `undefined` when\n * none of the enpilink/standard URL vars are set — in that case `pg` falls back\n * to its own `PG*` env-var handling. NEVER returns a hardcoded default.\n */\nexport function resolvePostgresConnectionString(): string | undefined {\n // Treat blank/whitespace-only values as unset so a blank var never shadows a\n // real one (and never becomes a hardcoded-looking default).\n const enpilink = process.env.ENPILINK_DB_URL?.trim();\n if (enpilink) {\n return enpilink;\n }\n const standard = process.env.DATABASE_URL?.trim();\n return standard ? standard : undefined;\n}\n\nexport class PostgresStorageAdapter implements StorageAdapter {\n private readonly injectedPool?: PgPoolLike;\n private readonly connectionString?: string;\n private pool: PgPoolLike | null = null;\n\n constructor(opts?: PostgresStorageOptions) {\n this.injectedPool = opts?.pool;\n this.connectionString =\n opts?.connectionString ?? resolvePostgresConnectionString();\n }\n\n async init(): Promise<void> {\n if (this.pool) {\n return;\n }\n if (this.injectedPool) {\n this.pool = this.injectedPool;\n } else {\n // Dynamic import keeps `pg` out of the load path until a postgres adapter\n // is actually instantiated (memory/sqlite users pay nothing).\n const mod = await import(\"pg\");\n const Pool = (mod.default?.Pool ??\n (mod as unknown as { Pool: new (cfg?: unknown) => PgPoolLike })\n .Pool) as new (\n cfg?: unknown,\n ) => PgPoolLike;\n // When no connectionString is resolved, pass none so `pg` reads PG* vars.\n this.pool = this.connectionString\n ? new Pool({ connectionString: this.connectionString })\n : new Pool();\n }\n await this.pool.query(SCHEMA);\n }\n\n private require(): PgPoolLike {\n if (!this.pool) {\n throw new Error(\"PostgresStorageAdapter: call init() before use\");\n }\n return this.pool;\n }\n\n async recordEvent(e: AnalyticsEvent): Promise<void> {\n const pool = this.require();\n await pool.query(\n \"INSERT INTO events (ts, type, tool, method, ms, ok, error, meta) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\",\n [\n e.ts,\n e.type,\n e.tool ?? null,\n e.method ?? null,\n e.ms ?? null,\n e.ok === undefined ? null : e.ok,\n e.error ?? null,\n e.meta === undefined ? null : JSON.stringify(e.meta),\n ],\n );\n }\n\n async queryEvents(f: EventQuery = {}): Promise<AnalyticsEvent[]> {\n const pool = this.require();\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.since !== undefined) {\n params.push(f.since);\n where.push(`ts >= $${params.length}`);\n }\n if (f.type !== undefined) {\n params.push(f.type);\n where.push(`type = $${params.length}`);\n }\n if (f.tool !== undefined) {\n params.push(f.tool);\n where.push(`tool = $${params.length}`);\n }\n const clause = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const limit = limitClause(f.limit, params);\n const { rows } = await pool.query<EventRow>(\n `SELECT ts, type, tool, method, ms, ok, error, meta FROM events ${clause} ORDER BY id DESC ${limit}`,\n params,\n );\n return rows.map(rowToEvent);\n }\n\n async appendLog(l: LogEntry): Promise<void> {\n const pool = this.require();\n await pool.query(\n \"INSERT INTO logs (ts, level, msg, data) VALUES ($1, $2, $3, $4)\",\n [\n l.ts,\n l.level,\n l.msg,\n l.data === undefined ? null : JSON.stringify(l.data),\n ],\n );\n }\n\n async queryLogs(f: LogQuery = {}): Promise<LogEntry[]> {\n const pool = this.require();\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.since !== undefined) {\n params.push(f.since);\n where.push(`ts >= $${params.length}`);\n }\n if (f.level !== undefined) {\n params.push(f.level);\n where.push(`level = $${params.length}`);\n }\n const clause = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const limit = limitClause(f.limit, params);\n const { rows } = await pool.query<LogRow>(\n `SELECT ts, level, msg, data FROM logs ${clause} ORDER BY id DESC ${limit}`,\n params,\n );\n return rows.map(rowToLog);\n }\n\n async getConfig(key: string): Promise<unknown> {\n const pool = this.require();\n const { rows } = await pool.query<{ value: string }>(\n \"SELECT value FROM config WHERE key = $1\",\n [key],\n );\n const row = rows[0];\n return row === undefined ? undefined : JSON.parse(row.value);\n }\n\n async setConfig(key: string, value: unknown, actor?: string): Promise<void> {\n const pool = this.require();\n const serialized = JSON.stringify(value);\n // Read-old → upsert-new → write-audit. pg-mem does not implement\n // transactions identically across versions, so we read-then-write without a\n // BEGIN/COMMIT wrapper; the audit row records the prior value either way.\n const { rows } = await pool.query<{ value: string }>(\n \"SELECT value FROM config WHERE key = $1\",\n [key],\n );\n const oldSerialized = rows[0]?.value ?? null;\n await pool.query(\n \"INSERT INTO config (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2\",\n [key, serialized],\n );\n await pool.query(\n \"INSERT INTO config_audit (ts, key, old_value, new_value, actor) VALUES ($1, $2, $3, $4, $5)\",\n [Date.now(), key, oldSerialized, serialized, actor ?? \"system\"],\n );\n }\n\n async clearConfig(key: string, actor?: string): Promise<void> {\n const pool = this.require();\n const { rows } = await pool.query<{ value: string }>(\n \"SELECT value FROM config WHERE key = $1\",\n [key],\n );\n const existing = rows[0];\n if (existing === undefined) {\n return;\n }\n await pool.query(\"DELETE FROM config WHERE key = $1\", [key]);\n // `new_value` is NOT NULL; a reset stores JSON `null` to represent\n // \"reset to default\" (getConfigAudit parses it back to `null`).\n await pool.query(\n \"INSERT INTO config_audit (ts, key, old_value, new_value, actor) VALUES ($1, $2, $3, $4, $5)\",\n [Date.now(), key, existing.value, \"null\", actor ?? \"system\"],\n );\n }\n\n async allConfig(): Promise<Record<string, unknown>> {\n const pool = this.require();\n const { rows } = await pool.query<{ key: string; value: string }>(\n \"SELECT key, value FROM config\",\n );\n const out: Record<string, unknown> = {};\n for (const r of rows) {\n out[r.key] = JSON.parse(r.value);\n }\n return out;\n }\n\n async getConfigAudit(): Promise<ConfigAuditEntry[]> {\n const pool = this.require();\n const { rows } = await pool.query<AuditRow>(\n \"SELECT ts, key, old_value, new_value, actor FROM config_audit ORDER BY id DESC\",\n );\n return rows.map((r) => ({\n ts: Number(r.ts),\n key: r.key,\n oldValue: r.old_value === null ? undefined : JSON.parse(r.old_value),\n newValue: JSON.parse(r.new_value),\n actor: r.actor,\n }));\n }\n\n async close(): Promise<void> {\n if (this.pool) {\n await this.pool.end();\n this.pool = null;\n }\n }\n}\n\nfunction limitClause(limit: number | undefined, params: unknown[]): string {\n if (limit === undefined || limit < 0) {\n return \"\";\n }\n params.push(limit);\n return `LIMIT $${params.length}`;\n}\n\ninterface EventRow {\n ts: number | string;\n type: string;\n tool: string | null;\n method: string | null;\n ms: number | string | null;\n ok: boolean | null;\n error: string | null;\n meta: string | null;\n}\n\ninterface LogRow {\n ts: number | string;\n level: string;\n msg: string;\n data: string | null;\n}\n\ninterface AuditRow {\n ts: number | string;\n key: string;\n old_value: string | null;\n new_value: string;\n actor: string;\n}\n\nfunction rowToEvent(r: EventRow): AnalyticsEvent {\n const e: AnalyticsEvent = { ts: Number(r.ts), type: r.type };\n if (r.tool !== null) {\n e.tool = r.tool;\n }\n if (r.method !== null) {\n e.method = r.method;\n }\n if (r.ms !== null) {\n e.ms = Number(r.ms);\n }\n if (r.ok !== null) {\n e.ok = r.ok;\n }\n if (r.error !== null) {\n e.error = r.error;\n }\n if (r.meta !== null) {\n e.meta = JSON.parse(r.meta);\n }\n return e;\n}\n\nfunction rowToLog(r: LogRow): LogEntry {\n const l: LogEntry = {\n ts: Number(r.ts),\n level: r.level as LogEntry[\"level\"],\n msg: r.msg,\n };\n if (r.data !== null) {\n l.data = JSON.parse(r.data);\n }\n return l;\n}\n\n/**\n * Schema. `BIGINT` for epoch-ms timestamps; `BIGSERIAL` ids give a stable\n * insertion order for most-recent-first queries (`ORDER BY id DESC`). `meta` /\n * `data` / config values are opaque JSON text.\n */\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n type TEXT NOT NULL,\n tool TEXT,\n method TEXT,\n ms BIGINT,\n ok BOOLEAN,\n error TEXT,\n meta TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events (type);\nCREATE INDEX IF NOT EXISTS idx_events_tool ON events (tool);\n\nCREATE TABLE IF NOT EXISTS logs (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n level TEXT NOT NULL,\n msg TEXT NOT NULL,\n data TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_logs_ts ON logs (ts);\nCREATE INDEX IF NOT EXISTS idx_logs_level ON logs (level);\n\nCREATE TABLE IF NOT EXISTS config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS config_audit (\n id BIGSERIAL PRIMARY KEY,\n ts BIGINT NOT NULL,\n key TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT NOT NULL,\n actor TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_audit_key ON config_audit (key);\n`;\n"]}