@graphorin/server 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +6 -6
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +40 -611
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +9 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +22 -26
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +6 -0
  59. package/dist/package.js.map +1 -0
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.d.ts.map +1 -1
  99. package/dist/ws/dispatcher.js +19 -12
  100. package/dist/ws/dispatcher.js.map +1 -1
  101. package/dist/ws/index.d.ts +2 -2
  102. package/dist/ws/index.js +3 -3
  103. package/dist/ws/replay-buffer.d.ts +35 -1
  104. package/dist/ws/replay-buffer.d.ts.map +1 -1
  105. package/dist/ws/replay-buffer.js +35 -3
  106. package/dist/ws/replay-buffer.js.map +1 -1
  107. package/dist/ws/upgrade.d.ts.map +1 -1
  108. package/dist/ws/upgrade.js +26 -19
  109. package/dist/ws/upgrade.js.map +1 -1
  110. package/package.json +31 -30
  111. package/src/app-audit-binding.ts +227 -0
  112. package/src/app-daemons.ts +73 -0
  113. package/src/app-lifecycle.ts +476 -0
  114. package/src/app-metrics.ts +144 -0
  115. package/src/app-middleware.ts +149 -0
  116. package/src/app-routes.ts +305 -0
  117. package/src/app-ws.ts +111 -0
  118. package/src/app.ts +317 -0
  119. package/src/commentary/audit-bridge.ts +135 -0
  120. package/src/commentary/built-in-patterns.ts +79 -0
  121. package/src/commentary/index.ts +32 -0
  122. package/src/commentary/sanitizer.ts +238 -0
  123. package/src/commentary/types.ts +130 -0
  124. package/src/config.ts +472 -0
  125. package/src/consolidator/daemon.ts +141 -0
  126. package/src/consolidator/index.ts +14 -0
  127. package/src/errors/index.ts +247 -0
  128. package/src/health/checks.ts +322 -0
  129. package/src/health/index.ts +34 -0
  130. package/src/health/routes.ts +154 -0
  131. package/src/index.ts +243 -0
  132. package/src/internal/context.ts +77 -0
  133. package/src/internal/ids.ts +17 -0
  134. package/src/internal/json.ts +47 -0
  135. package/src/internal/wire-error.ts +44 -0
  136. package/src/lifecycle/hooks.ts +66 -0
  137. package/src/lifecycle/index.ts +16 -0
  138. package/src/lifecycle/pre-bind.ts +138 -0
  139. package/src/metrics/catalog.ts +112 -0
  140. package/src/metrics/index.ts +12 -0
  141. package/src/metrics/registry.ts +337 -0
  142. package/src/metrics/tools-bridge.ts +124 -0
  143. package/src/middleware/audit.ts +114 -0
  144. package/src/middleware/auth.ts +195 -0
  145. package/src/middleware/cors.ts +72 -0
  146. package/src/middleware/csrf.ts +98 -0
  147. package/src/middleware/idempotency.ts +336 -0
  148. package/src/middleware/index.ts +38 -0
  149. package/src/middleware/rate-limit.ts +71 -0
  150. package/src/middleware/request-state.ts +79 -0
  151. package/src/middleware/scope.ts +161 -0
  152. package/src/registry/index.ts +278 -0
  153. package/src/replay/index.ts +14 -0
  154. package/src/replay/routes.ts +255 -0
  155. package/src/routes/agents.ts +363 -0
  156. package/src/routes/audit.ts +207 -0
  157. package/src/routes/auth.ts +80 -0
  158. package/src/routes/health.ts +42 -0
  159. package/src/routes/index.ts +16 -0
  160. package/src/routes/mcp.ts +97 -0
  161. package/src/routes/memory.ts +152 -0
  162. package/src/routes/sessions.ts +250 -0
  163. package/src/routes/skills.ts +91 -0
  164. package/src/routes/tokens.ts +166 -0
  165. package/src/routes/workflows.ts +616 -0
  166. package/src/runtime/retention.ts +284 -0
  167. package/src/runtime/run-state.ts +422 -0
  168. package/src/sse/events.ts +303 -0
  169. package/src/sse/index.ts +7 -0
  170. package/src/tools-audit-bridge.ts +120 -0
  171. package/src/triggers/daemon.ts +198 -0
  172. package/src/triggers/index.ts +16 -0
  173. package/src/triggers/routes.ts +139 -0
  174. package/src/workflows/timer-daemon.ts +102 -0
  175. package/src/ws/dispatcher.ts +537 -0
  176. package/src/ws/index.ts +45 -0
  177. package/src/ws/replay-buffer.ts +209 -0
  178. package/src/ws/subjects.ts +162 -0
  179. package/src/ws/ticket.ts +174 -0
  180. package/src/ws/upgrade.ts +507 -0
  181. package/dist/lifecycle/index.js +0 -3
  182. package/dist/routes/index.js +0 -12
package/src/config.ts ADDED
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Strongly-typed configuration loader for the Graphorin server.
3
+ *
4
+ * Operators ship a `graphorin.config.ts` (or `.js` / `.mjs` / JSON)
5
+ * file that calls {@link defineConfig} to construct a typed config
6
+ * object; the server runtime accepts either the raw object, a path
7
+ * to such a file, or a function that returns one.
8
+ *
9
+ * Validation is performed via Zod so user-facing errors include the
10
+ * exact path of every failing field, not just a generic
11
+ * "TypeError: undefined".
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ import { z } from 'zod';
17
+
18
+ import { ConfigInvalidError } from './errors/index.js';
19
+
20
+ /**
21
+ * String literal that flags a value as a `SecretRef` URI. The
22
+ * server's pre-bind step resolves every `*Ref` field through the
23
+ * `@graphorin/security` resolver registry before binding the
24
+ * listener; an unresolvable ref fails fast with
25
+ * `PrebindSecretUnresolvableError`.
26
+ *
27
+ * @stable
28
+ */
29
+ export type SecretRefString = string;
30
+
31
+ /**
32
+ * Selector for which `SecretsStore` flavour the server activates.
33
+ * Mirrors `--secrets-source` from DEC-136.
34
+ *
35
+ * @stable
36
+ */
37
+ export type SecretsSource = 'auto' | 'keyring' | 'encrypted-file' | 'env';
38
+
39
+ /** @stable */
40
+ export type IdempotencyRequireKeyMode = 'off' | 'warn' | 'enforce';
41
+
42
+ /** @stable */
43
+ export type DeliveryCommentaryPolicyConfig = 'wrap' | 'strip' | 'pass-through';
44
+
45
+ /** @stable */
46
+ export interface ServerConfigSpec {
47
+ readonly server: {
48
+ readonly host: string;
49
+ readonly port: number;
50
+ readonly basePath: string;
51
+ readonly cors: {
52
+ readonly allowOrigins: ReadonlyArray<string>;
53
+ readonly allowCredentials: boolean;
54
+ readonly allowMethods: ReadonlyArray<string>;
55
+ readonly allowHeaders: ReadonlyArray<string>;
56
+ readonly maxAgeSeconds: number;
57
+ };
58
+ readonly csrf: {
59
+ readonly enabled: boolean;
60
+ readonly cookieName: string;
61
+ readonly headerName: string;
62
+ readonly safeMethods: ReadonlyArray<string>;
63
+ };
64
+ readonly rateLimit: {
65
+ readonly enabled: boolean;
66
+ readonly windowMs: number;
67
+ readonly perIpRequests: number;
68
+ };
69
+ readonly idempotency: {
70
+ readonly enabled: boolean;
71
+ readonly requireKey: IdempotencyRequireKeyMode;
72
+ readonly ttlSeconds: number;
73
+ readonly checkBodyFingerprint: boolean;
74
+ readonly lruCacheSize: number;
75
+ };
76
+ readonly shutdown: {
77
+ readonly drainTimeoutMs: number;
78
+ };
79
+ readonly trustProxy: boolean;
80
+ readonly stream: {
81
+ readonly disconnectPolicy: 'continue' | 'pause-on-disconnect' | 'abort-on-disconnect';
82
+ readonly disconnectGracePeriodMs: number;
83
+ readonly replayBuffer: {
84
+ readonly maxEvents: number;
85
+ readonly ttlSeconds: number;
86
+ readonly pruneIntervalSeconds: number;
87
+ };
88
+ readonly perConnectionQueueLimit: number;
89
+ };
90
+ readonly ws: {
91
+ readonly enabled: boolean;
92
+ readonly path: string;
93
+ readonly ticketTtlMs: number;
94
+ readonly commentarySanitization: {
95
+ readonly policy: DeliveryCommentaryPolicyConfig;
96
+ readonly applyToEvents: ReadonlyArray<string>;
97
+ };
98
+ };
99
+ readonly sse: {
100
+ readonly enabled: boolean;
101
+ readonly path: string;
102
+ readonly keepAliveMs: number;
103
+ };
104
+ };
105
+ readonly storage: {
106
+ readonly path: string;
107
+ readonly mode: 'lib' | 'server';
108
+ readonly walCheckpointIntervalMs?: number;
109
+ readonly encryption: {
110
+ readonly enabled: boolean;
111
+ readonly cipher?: string;
112
+ readonly passphraseRef?: SecretRefString;
113
+ };
114
+ };
115
+ readonly retention: {
116
+ readonly enabled: boolean;
117
+ readonly intervalMs: number;
118
+ readonly spansDays: number;
119
+ readonly consolidatorRunsDays: number;
120
+ readonly dlqExhaustedDays: number;
121
+ readonly idempotency: boolean;
122
+ readonly sessionsDays?: number;
123
+ readonly sessionsClosedOnly: boolean;
124
+ readonly memoryHistoryDays?: number;
125
+ readonly workflowThreadsDays?: number;
126
+ readonly auditDays?: number;
127
+ };
128
+ readonly audit: {
129
+ readonly enabled: boolean;
130
+ readonly path?: string;
131
+ readonly passphraseRef?: SecretRefString;
132
+ readonly cipher?: string;
133
+ /**
134
+ * W-051: which tools/MCP audit-bus events land in the audit chain.
135
+ * `'security'` (default) writes the security-significant subset
136
+ * (dataflow flagged/blocked/declassified, sanitization, approvals,
137
+ * collisions, cap-disabled); `'all'` adds per-call
138
+ * `tool:execute:*` chatter; `'off'` disables the bridge.
139
+ */
140
+ readonly toolEvents: 'security' | 'all' | 'off';
141
+ };
142
+ readonly secrets: {
143
+ readonly source: SecretsSource;
144
+ readonly strict: boolean;
145
+ };
146
+ readonly auth: {
147
+ readonly kind: 'token' | 'none';
148
+ readonly pepperRef?: SecretRefString;
149
+ readonly tokenPrefix: string;
150
+ readonly tokenEnvironments: ReadonlyArray<string>;
151
+ readonly perIpFailureThreshold?: number;
152
+ readonly perIpLockoutMs?: number;
153
+ };
154
+ readonly observability: {
155
+ readonly logger: 'json' | 'pretty' | 'silent';
156
+ };
157
+ readonly hardening: {
158
+ readonly applyOnStart: boolean;
159
+ readonly refuseRoot: boolean;
160
+ readonly umask: number;
161
+ };
162
+ readonly metrics: {
163
+ readonly enabled: boolean;
164
+ readonly path: string;
165
+ readonly requireAuth: boolean;
166
+ };
167
+ readonly health: {
168
+ readonly walWarnThresholdBytes: number;
169
+ };
170
+ }
171
+
172
+ const corsSchema = z
173
+ .object({
174
+ allowOrigins: z.array(z.string()).default([]),
175
+ allowCredentials: z.boolean().default(false),
176
+ allowMethods: z.array(z.string()).default(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
177
+ allowHeaders: z
178
+ .array(z.string())
179
+ .default(['Authorization', 'Content-Type', 'Idempotency-Key', 'X-CSRF-Token']),
180
+ maxAgeSeconds: z.number().int().nonnegative().default(600),
181
+ })
182
+ .strict()
183
+ .default({});
184
+
185
+ const csrfSchema = z
186
+ .object({
187
+ enabled: z.boolean().default(true),
188
+ cookieName: z.string().default('graphorin_csrf'),
189
+ headerName: z.string().default('X-CSRF-Token'),
190
+ safeMethods: z.array(z.string()).default(['GET', 'HEAD', 'OPTIONS']),
191
+ })
192
+ .strict()
193
+ .default({});
194
+
195
+ const rateLimitSchema = z
196
+ .object({
197
+ enabled: z.boolean().default(true),
198
+ windowMs: z.number().int().positive().default(60_000),
199
+ perIpRequests: z.number().int().positive().default(60),
200
+ })
201
+ .strict()
202
+ .default({});
203
+
204
+ const idempotencySchema = z
205
+ .object({
206
+ enabled: z.boolean().default(true),
207
+ requireKey: z.enum(['off', 'warn', 'enforce']).default('warn'),
208
+ ttlSeconds: z.number().int().positive().default(86_400),
209
+ checkBodyFingerprint: z.boolean().default(true),
210
+ lruCacheSize: z.number().int().positive().default(1_000),
211
+ })
212
+ .strict()
213
+ .default({});
214
+
215
+ const shutdownSchema = z
216
+ .object({
217
+ drainTimeoutMs: z.number().int().positive().default(30_000),
218
+ })
219
+ .strict()
220
+ .default({});
221
+
222
+ const streamReplayBufferSchema = z
223
+ .object({
224
+ maxEvents: z.number().int().positive().default(1_000),
225
+ ttlSeconds: z.number().int().positive().default(300),
226
+ // W-028: interval of the periodic TTL sweep that releases
227
+ // finished-run subjects with no further push/replay activity.
228
+ pruneIntervalSeconds: z.number().int().positive().default(60),
229
+ })
230
+ .strict()
231
+ .default({});
232
+
233
+ const streamSchema = z
234
+ .object({
235
+ disconnectPolicy: z
236
+ .enum(['continue', 'pause-on-disconnect', 'abort-on-disconnect'])
237
+ .default('continue'),
238
+ disconnectGracePeriodMs: z.number().int().nonnegative().default(10_000),
239
+ replayBuffer: streamReplayBufferSchema,
240
+ perConnectionQueueLimit: z.number().int().positive().default(1_000),
241
+ })
242
+ .strict()
243
+ .default({});
244
+
245
+ const wsCommentarySchema = z
246
+ .object({
247
+ policy: z.enum(['wrap', 'strip', 'pass-through']).default('wrap'),
248
+ applyToEvents: z
249
+ .array(z.string().min(1))
250
+ .default(['tool.execute.end', 'tool.execute.error', 'text.delta']),
251
+ })
252
+ .strict()
253
+ .default({});
254
+
255
+ const wsSchema = z
256
+ .object({
257
+ enabled: z.boolean().default(true),
258
+ path: z.string().default('/ws'),
259
+ ticketTtlMs: z
260
+ .number()
261
+ .int()
262
+ .positive()
263
+ .default(5 * 60_000),
264
+ commentarySanitization: wsCommentarySchema,
265
+ })
266
+ .strict()
267
+ .default({});
268
+
269
+ const sseSchema = z
270
+ .object({
271
+ enabled: z.boolean().default(true),
272
+ path: z.string().default('/sessions'),
273
+ keepAliveMs: z.number().int().positive().default(15_000),
274
+ })
275
+ .strict()
276
+ .default({});
277
+
278
+ const serverSchema = z
279
+ .object({
280
+ host: z.string().min(1).default('127.0.0.1'),
281
+ port: z.number().int().min(0).max(65_535).default(8_080),
282
+ basePath: z.string().default('/v1'),
283
+ cors: corsSchema,
284
+ csrf: csrfSchema,
285
+ rateLimit: rateLimitSchema,
286
+ idempotency: idempotencySchema,
287
+ shutdown: shutdownSchema,
288
+ trustProxy: z.boolean().default(false),
289
+ stream: streamSchema,
290
+ ws: wsSchema,
291
+ sse: sseSchema,
292
+ })
293
+ .strict()
294
+ .default({});
295
+
296
+ const encryptionSchema = z
297
+ .object({
298
+ enabled: z.boolean().default(false),
299
+ cipher: z.string().optional(),
300
+ passphraseRef: z.string().optional(),
301
+ })
302
+ .strict()
303
+ .default({});
304
+
305
+ const storageSchema = z
306
+ .object({
307
+ path: z.string().min(1).default('./.graphorin/data.db'),
308
+ mode: z.enum(['lib', 'server']).default('server'),
309
+ walCheckpointIntervalMs: z.number().int().positive().optional(),
310
+ encryption: encryptionSchema,
311
+ })
312
+ .strict()
313
+ .default({});
314
+
315
+ /**
316
+ * W-010 / W-008 retention policy. Default-on ONLY for derived or
317
+ * recoverable data (span telemetry, consolidator run counters, the
318
+ * exhausted consolidator DLQ, expired idempotency response bodies).
319
+ * Primary user content - sessions, memory history, workflow threads,
320
+ * the session audit trail - is never touched unless the operator sets
321
+ * the matching `*Days` window explicitly.
322
+ */
323
+ const retentionSchema = z
324
+ .object({
325
+ enabled: z.boolean().default(true),
326
+ intervalMs: z
327
+ .number()
328
+ .int()
329
+ .positive()
330
+ .default(6 * 60 * 60 * 1000),
331
+ spansDays: z.number().positive().default(30),
332
+ consolidatorRunsDays: z.number().positive().default(90),
333
+ dlqExhaustedDays: z.number().positive().default(30),
334
+ idempotency: z.boolean().default(true),
335
+ sessionsDays: z.number().positive().optional(),
336
+ sessionsClosedOnly: z.boolean().default(true),
337
+ memoryHistoryDays: z.number().positive().optional(),
338
+ workflowThreadsDays: z.number().positive().optional(),
339
+ auditDays: z.number().positive().optional(),
340
+ })
341
+ .strict()
342
+ .default({});
343
+
344
+ const auditSchema = z
345
+ .object({
346
+ enabled: z.boolean().default(false),
347
+ path: z.string().optional(),
348
+ passphraseRef: z.string().optional(),
349
+ cipher: z.string().optional(),
350
+ // W-051: tools/MCP audit-bus bridge volume policy.
351
+ toolEvents: z.enum(['security', 'all', 'off']).default('security'),
352
+ })
353
+ .strict()
354
+ .default({});
355
+
356
+ const secretsSchema = z
357
+ .object({
358
+ source: z.enum(['auto', 'keyring', 'encrypted-file', 'env']).default('auto'),
359
+ strict: z.boolean().default(false),
360
+ })
361
+ .strict()
362
+ .default({});
363
+
364
+ const authSchema = z
365
+ .object({
366
+ kind: z.enum(['token', 'none']).default('token'),
367
+ pepperRef: z.string().optional(),
368
+ tokenPrefix: z.string().default('gph'),
369
+ tokenEnvironments: z.array(z.string()).default(['live', 'test', 'local']),
370
+ perIpFailureThreshold: z.number().int().positive().optional(),
371
+ perIpLockoutMs: z.number().int().positive().optional(),
372
+ })
373
+ .strict()
374
+ .default({});
375
+
376
+ const observabilitySchema = z
377
+ .object({
378
+ logger: z.enum(['json', 'pretty', 'silent']).default('json'),
379
+ })
380
+ .strict()
381
+ .default({});
382
+
383
+ const hardeningSchema = z
384
+ .object({
385
+ applyOnStart: z.boolean().default(true),
386
+ refuseRoot: z.boolean().default(true),
387
+ umask: z.number().int().nonnegative().default(0o077),
388
+ })
389
+ .strict()
390
+ .default({});
391
+
392
+ const metricsSchema = z
393
+ .object({
394
+ enabled: z.boolean().default(true),
395
+ path: z.string().default('/metrics'),
396
+ requireAuth: z.boolean().default(false),
397
+ })
398
+ .strict()
399
+ .default({});
400
+
401
+ const healthSchema = z
402
+ .object({
403
+ walWarnThresholdBytes: z
404
+ .number()
405
+ .int()
406
+ .positive()
407
+ .default(50 * 1024 * 1024),
408
+ })
409
+ .strict()
410
+ .default({});
411
+
412
+ /**
413
+ * Zod schema for the resolved {@link ServerConfigSpec}. Exposed for
414
+ * advanced users that want to validate other config sources (env-only
415
+ * launch, CLI overrides, etc.).
416
+ *
417
+ * @stable
418
+ */
419
+ export const ServerConfigSchema = z
420
+ .object({
421
+ server: serverSchema,
422
+ storage: storageSchema,
423
+ retention: retentionSchema,
424
+ audit: auditSchema,
425
+ secrets: secretsSchema,
426
+ auth: authSchema,
427
+ observability: observabilitySchema,
428
+ hardening: hardeningSchema,
429
+ metrics: metricsSchema,
430
+ health: healthSchema,
431
+ })
432
+ .strict()
433
+ .default({});
434
+
435
+ /**
436
+ * Input shape accepted by {@link defineConfig}. Every field is
437
+ * optional; missing values fall back to a documented default.
438
+ *
439
+ * @stable
440
+ */
441
+ export type ServerConfigInput = z.input<typeof ServerConfigSchema>;
442
+
443
+ /**
444
+ * Helper for `graphorin.config.ts` files. Pure pass-through that
445
+ * provides editor autocomplete; the actual parsing happens at server
446
+ * startup so callers always see the same error path regardless of
447
+ * which loader (TS / JS / JSON) the operator picked.
448
+ *
449
+ * @stable
450
+ */
451
+ export function defineConfig(input: ServerConfigInput): ServerConfigInput {
452
+ return input;
453
+ }
454
+
455
+ /**
456
+ * Parse + validate user input. Returns a strongly-typed
457
+ * {@link ServerConfigSpec}; throws {@link ConfigInvalidError} on
458
+ * any invalid field with a flattened issue list.
459
+ *
460
+ * @stable
461
+ */
462
+ export function parseServerConfig(input: unknown): ServerConfigSpec {
463
+ const result = ServerConfigSchema.safeParse(input ?? {});
464
+ if (!result.success) {
465
+ const issues = result.error.issues.map((issue) => ({
466
+ path: issue.path,
467
+ message: issue.message,
468
+ }));
469
+ throw new ConfigInvalidError(issues, result.error);
470
+ }
471
+ return result.data as unknown as ServerConfigSpec;
472
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Lifecycle wrapper around an externally-built `Consolidator`. Phase
3
+ * 14c wires the runtime into `beforeStart` / `beforeShutdown` so the
4
+ * background pipeline starts and stops cleanly with the daemon. The
5
+ * adapter does not own the consolidator construction - operators
6
+ * supply the instance through the `createServer({ consolidator })`
7
+ * option so the framework stays decoupled from the heavy
8
+ * `@graphorin/memory` package.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ /**
14
+ * Structurally-typed view of the `@graphorin/memory` Consolidator
15
+ * surface. Importing the full type would force a hard dependency on
16
+ * `@graphorin/memory`; the structural subset captured here is enough
17
+ * for the lifecycle integration + the `/v1/health` aggregator.
18
+ *
19
+ * @stable
20
+ */
21
+ export interface ConsolidatorLike {
22
+ start(): Promise<void>;
23
+ stop(): Promise<void>;
24
+ status(): Promise<ConsolidatorStatusLike>;
25
+ setTier?(tier: string): Promise<void>;
26
+ pause?(): Promise<void>;
27
+ resume?(): Promise<void>;
28
+ drainDlq?(): Promise<number>;
29
+ /**
30
+ * Register the consolidator's cron / idle triggers with the server's
31
+ * triggers scheduler so background consolidation actually fires (MCON-4).
32
+ * Called from `beforeStart` when both a consolidator and a triggers daemon
33
+ * are configured. Optional so a consolidator without a `defaultScope` (or a
34
+ * custom surface) simply opts out.
35
+ */
36
+ registerWithScheduler?(scheduler: import('@graphorin/triggers').Scheduler): Promise<unknown>;
37
+ }
38
+
39
+ /**
40
+ * Subset of `ConsolidatorStatus` the server health endpoint and the
41
+ * Prometheus metrics consume. The full struct lives in
42
+ * `@graphorin/memory/consolidator`.
43
+ *
44
+ * @stable
45
+ */
46
+ export interface ConsolidatorStatusLike {
47
+ readonly tier: string;
48
+ readonly running: boolean;
49
+ readonly paused: boolean;
50
+ readonly queueDepth: number;
51
+ readonly dlqSize: number;
52
+ readonly deferredRuns: number;
53
+ readonly emptyExtractions: number;
54
+ readonly budget: {
55
+ readonly tokensUsedToday: number;
56
+ readonly costUsedToday: number;
57
+ readonly tokensRemaining: number;
58
+ readonly costRemaining: number;
59
+ readonly resetAt: string;
60
+ };
61
+ }
62
+
63
+ /**
64
+ * @stable
65
+ */
66
+ export interface CreateConsolidatorDaemonOptions {
67
+ readonly consolidator: ConsolidatorLike;
68
+ /** Hard timeout on `consolidator.stop()`. Defaults to 10 s. */
69
+ readonly stopTimeoutMs?: number;
70
+ readonly warn?: (message: string) => void;
71
+ }
72
+
73
+ /**
74
+ * @stable
75
+ */
76
+ export interface ConsolidatorDaemon {
77
+ start(): Promise<void>;
78
+ stop(): Promise<void>;
79
+ status(): Promise<ConsolidatorStatusLike>;
80
+ readonly consolidator: ConsolidatorLike;
81
+ }
82
+
83
+ /**
84
+ * @stable
85
+ */
86
+ export function createConsolidatorDaemon(
87
+ options: CreateConsolidatorDaemonOptions,
88
+ ): ConsolidatorDaemon {
89
+ const stopTimeoutMs = options.stopTimeoutMs ?? 10_000;
90
+ const warn = options.warn ?? ((m: string) => process.stderr.write(`${m}\n`));
91
+ let started = false;
92
+
93
+ return {
94
+ get consolidator() {
95
+ return options.consolidator;
96
+ },
97
+ async start() {
98
+ if (started) return;
99
+ started = true;
100
+ await options.consolidator.start();
101
+ },
102
+ async stop() {
103
+ if (!started) return;
104
+ started = false;
105
+ try {
106
+ await withTimeout(options.consolidator.stop(), stopTimeoutMs);
107
+ } catch (err) {
108
+ warn(
109
+ `[graphorin/server] consolidator daemon: stop() exceeded ${stopTimeoutMs}ms; force-aborting (${describeError(err)}).`,
110
+ );
111
+ }
112
+ },
113
+ async status() {
114
+ return options.consolidator.status();
115
+ },
116
+ };
117
+ }
118
+
119
+ async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
120
+ return new Promise<T>((resolve, reject) => {
121
+ const timer = setTimeout(() => {
122
+ reject(new Error(`timeout after ${ms}ms`));
123
+ }, ms);
124
+ p.then(
125
+ (value) => {
126
+ clearTimeout(timer);
127
+ resolve(value);
128
+ },
129
+ (err) => {
130
+ clearTimeout(timer);
131
+ reject(err);
132
+ },
133
+ );
134
+ });
135
+ }
136
+
137
+ function describeError(err: unknown): string {
138
+ if (err === null || err === undefined) return 'unknown';
139
+ if (err instanceof Error) return err.message;
140
+ return String(err);
141
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@graphorin/server/consolidator` - Phase 14c lifecycle adapter for
3
+ * the consolidator runtime.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ export {
9
+ type ConsolidatorDaemon,
10
+ type ConsolidatorLike,
11
+ type ConsolidatorStatusLike,
12
+ type CreateConsolidatorDaemonOptions,
13
+ createConsolidatorDaemon,
14
+ } from './daemon.js';