@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/app.ts ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * `createServer({...})` - the single programmatic entry point.
3
+ *
4
+ * The factory returns a {@link GraphorinServer} handle the operator
5
+ * controls via `start()` / `stop()`. The same handle is consumed by
6
+ * the `graphorin start` CLI binary in `@graphorin/cli`.
7
+ *
8
+ * Phase 14a covers the full HTTP surface, the lifecycle plumbing,
9
+ * the Hono app composition, and the auth / scope / idempotency /
10
+ * audit middleware stack. Phase 14b/c attach the WebSocket layer +
11
+ * triggers daemon + observability endpoints to the same handle.
12
+ *
13
+ * This file is the composition root: the cohesive builders live in
14
+ * focused sibling modules - `app-audit-binding.ts` (default audit-db
15
+ * binding), `app-middleware.ts` (global + authenticated-subtree
16
+ * middleware), `app-ws.ts` (WebSocket layer), `app-daemons.ts`
17
+ * (triggers / consolidator daemon construction), `app-routes.ts`
18
+ * (route mounting), `app-metrics.ts` (live scrape refresh), and
19
+ * `app-lifecycle.ts` (`start()` / `stop()` sequencing).
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+
24
+ import process from 'node:process';
25
+ import { createSqliteStore, type GraphorinSqliteStore } from '@graphorin/store-sqlite';
26
+ import { Hono } from 'hono';
27
+ import { buildDaemons, type TriggersDaemonInput, type WorkflowTimersInput } from './app-daemons.js';
28
+ import { createLifecycle } from './app-lifecycle.js';
29
+ import { attachGlobalMiddleware } from './app-middleware.js';
30
+ import { buildWsLayer } from './app-ws.js';
31
+ import { parseServerConfig, type ServerConfigInput, type ServerConfigSpec } from './config.js';
32
+ import type { ConsolidatorDaemon, ConsolidatorLike } from './consolidator/daemon.js';
33
+ import { ConfigInvalidError } from './errors/index.js';
34
+ import type { HealthCheckOptions } from './health/index.js';
35
+ import type { ServerVariables } from './internal/context.js';
36
+ import type { LifecycleHooks } from './lifecycle/index.js';
37
+ import { createServerMetricRegistry, SERVER_METRIC_NAMES } from './metrics/catalog.js';
38
+ import type { MetricRegistry } from './metrics/registry.js';
39
+ import { AgentRegistry, WorkflowRegistry } from './registry/index.js';
40
+ import type { ReplayApi } from './replay/index.js';
41
+ import type { AuditApi, McpApi, MemoryApi, SessionApi, SkillsApi } from './routes/index.js';
42
+ import { RunStateTracker } from './runtime/run-state.js';
43
+ import type { TriggersDaemon } from './triggers/daemon.js';
44
+ import type { WorkflowTimerDaemon } from './workflows/timer-daemon.js';
45
+ import type { WsDispatcher, WsTicketStore } from './ws/index.js';
46
+
47
+ // Stable re-exports - both were born in this module and stay
48
+ // importable from `./app.js` (and the package barrel) unchanged.
49
+ export { ensureStoreAuditBinding } from './app-audit-binding.js';
50
+ export type { TriggersDaemonInput, WorkflowTimersInput } from './app-daemons.js';
51
+
52
+ /**
53
+ * Public surface returned by {@link createServer}.
54
+ *
55
+ * @stable
56
+ */
57
+ export interface GraphorinServer {
58
+ readonly version: string;
59
+ readonly config: ServerConfigSpec;
60
+ readonly app: Hono<{ Variables: ServerVariables }>;
61
+ readonly agents: AgentRegistry;
62
+ readonly workflows: WorkflowRegistry;
63
+ readonly runs: RunStateTracker;
64
+ readonly listeningOn: { readonly host: string; readonly port: number } | undefined;
65
+ /**
66
+ * Optional WebSocket dispatcher exposed when `server.ws.enabled =
67
+ * true`. Route handlers + the agent / workflow runtimes call
68
+ * `dispatcher.emit(subject, event)` to fan out events to subscribed
69
+ * clients. Returns `undefined` when WS is disabled.
70
+ */
71
+ readonly wsDispatcher: WsDispatcher | undefined;
72
+ /**
73
+ * Optional WebSocket ticket store exposed when `server.ws.enabled
74
+ * = true`. Surfaced primarily for tests; the
75
+ * `POST /v1/session/ws-ticket` route uses it transparently.
76
+ */
77
+ readonly wsTickets: WsTicketStore | undefined;
78
+ /**
79
+ * Optional triggers daemon - populated when the operator wired a
80
+ * scheduler (or an in-process trigger surface) at construction
81
+ * time. Phase 14c integration.
82
+ */
83
+ readonly triggers: TriggersDaemon | undefined;
84
+ /**
85
+ * Optional consolidator daemon - populated when the operator
86
+ * supplied a `Consolidator` instance via `createServer({
87
+ * consolidator })`. Phase 14c integration.
88
+ */
89
+ readonly consolidator: ConsolidatorDaemon | undefined;
90
+ /**
91
+ * W-032: optional workflow durable-timer daemon - populated when the
92
+ * operator wired a `createTimerDriver(...)` at construction time.
93
+ */
94
+ readonly workflowTimers: WorkflowTimerDaemon | undefined;
95
+ /**
96
+ * Phase 14c Prometheus registry. Always present; sample updates
97
+ * are observable via `metrics.snapshot()`.
98
+ */
99
+ readonly metrics: MetricRegistry;
100
+ start(): Promise<{ readonly host: string; readonly port: number }>;
101
+ stop(options?: { readonly force?: boolean }): Promise<void>;
102
+ }
103
+
104
+ /**
105
+ * @stable
106
+ */
107
+ export interface CreateServerOptions {
108
+ /** Loaded `graphorin.config.ts` payload - see `defineConfig({...})`. */
109
+ readonly config?: ServerConfigInput;
110
+ /**
111
+ * Optional pre-validated config. When supplied, `config` is ignored
112
+ * and the schema validation step is skipped. Useful for tests + the
113
+ * `graphorin migrate` CLI command which bypasses the listener.
114
+ */
115
+ readonly validatedConfig?: ServerConfigSpec;
116
+ /** Pre-built SQLite store. Tests inject an in-memory store. */
117
+ readonly store?: GraphorinSqliteStore;
118
+ /** Optional pre-built tracker. Tests inject deterministic timing. */
119
+ readonly runs?: RunStateTracker;
120
+ /** Optional pre-built registries. */
121
+ readonly agents?: AgentRegistry;
122
+ readonly workflows?: WorkflowRegistry;
123
+ /** Optional in-process domain adapters wired into REST routes. */
124
+ readonly sessions?: SessionApi;
125
+ readonly memory?: MemoryApi;
126
+ readonly skills?: SkillsApi;
127
+ readonly mcp?: McpApi;
128
+ readonly audit?: AuditApi;
129
+ /**
130
+ * Optional consolidator surface (`@graphorin/memory`). Phase 14c
131
+ * starts/stops the runtime alongside the server lifecycle and
132
+ * surfaces its status through `/v1/health`.
133
+ */
134
+ readonly consolidator?: ConsolidatorLike;
135
+ /**
136
+ * Optional triggers daemon - pass an existing one (e.g. built
137
+ * from `createScheduler`) or a triggers configuration the server
138
+ * should wrap with the daemon adapter.
139
+ */
140
+ readonly triggers?: TriggersDaemonInput;
141
+ /**
142
+ * W-032: optional workflow durable-timer surface - pass a
143
+ * `createTimerDriver(...)` built over your workflows + checkpoint
144
+ * stores (`{ driver }`), or a pre-built daemon. The server starts
145
+ * and stops it with the lifecycle and reports it on `/v1/health`.
146
+ */
147
+ readonly workflowTimers?: WorkflowTimersInput;
148
+ /**
149
+ * Optional replay API consumed by the scope-enforced replay
150
+ * endpoints. Phase 14c.
151
+ */
152
+ readonly replay?: ReplayApi;
153
+ /**
154
+ * Optional probes that augment `/v1/health`. Provided by consumer
155
+ * code (e.g. `embedder` provides `embedderLoaded`).
156
+ */
157
+ readonly healthProbes?: () => HealthCheckOptions | Promise<HealthCheckOptions>;
158
+ /**
159
+ * Optional Prometheus metric registry override. When omitted, the
160
+ * server constructs the canonical registry from
161
+ * {@link createServerMetricRegistry}.
162
+ */
163
+ readonly metricRegistry?: MetricRegistry;
164
+ /** Lifecycle hook overrides. */
165
+ readonly hooks?: LifecycleHooks;
166
+ /** Wall-clock provider for tests. */
167
+ readonly now?: () => number;
168
+ /** Override the cipher peer probe. Tests inject a stub. */
169
+ readonly probeCipherPeer?: () => Promise<void>;
170
+ /** Override the package version reported on `/v1/health`. */
171
+ readonly version?: string;
172
+ /** Skip `applyProcessHardening` (tests). */
173
+ readonly skipHardening?: boolean;
174
+ /** Skip starting the actual listener (tests). */
175
+ readonly skipListen?: boolean;
176
+ }
177
+
178
+ /**
179
+ * @stable
180
+ */
181
+ import pkg from '../package.json' with { type: 'json' };
182
+
183
+ export const VERSION: string = pkg.version;
184
+
185
+ /**
186
+ * Build a fully-wired Graphorin server. The returned handle is
187
+ * inert until `start()` is awaited.
188
+ *
189
+ * @stable
190
+ */
191
+ export async function createServer(options: CreateServerOptions = {}): Promise<GraphorinServer> {
192
+ const config: ServerConfigSpec =
193
+ options.validatedConfig ?? parseServerConfig(options.config ?? {});
194
+ const now = options.now ?? Date.now;
195
+ const startedAt = now();
196
+ const version = options.version ?? VERSION;
197
+
198
+ // IP-1: when encryption is configured, resolve the passphrase ref
199
+ // BEFORE constructing the store and thread the encryption config
200
+ // through - `graphorin init --encrypted` produced a config nothing
201
+ // honoured, and a database encrypted via `storage encrypt` could not
202
+ // be opened by the server at all.
203
+ let storeEncryption:
204
+ | { enabled: true; cipher?: never; passphraseResolver: () => Promise<string> }
205
+ | undefined;
206
+ if (options.store === undefined && config.storage.encryption.enabled) {
207
+ if (config.storage.encryption.passphraseRef === undefined) {
208
+ throw new Error(
209
+ '[graphorin/server] storage.encryption.enabled is true but no passphraseRef is configured.',
210
+ );
211
+ }
212
+ const { resolveSecret } = await import('@graphorin/security/secrets');
213
+ const passphrase = await resolveSecret(config.storage.encryption.passphraseRef);
214
+ storeEncryption = {
215
+ enabled: true,
216
+ passphraseResolver: async () => passphrase.use((v) => v),
217
+ } as never;
218
+ }
219
+ const store =
220
+ options.store ??
221
+ (await createSqliteStore({
222
+ path: config.storage.path,
223
+ mode: config.storage.mode,
224
+ ...(storeEncryption !== undefined
225
+ ? {
226
+ encryption: {
227
+ enabled: true,
228
+ ...(config.storage.encryption.cipher !== undefined
229
+ ? { cipher: config.storage.encryption.cipher as never }
230
+ : {}),
231
+ passphraseResolver: storeEncryption.passphraseResolver,
232
+ },
233
+ }
234
+ : {}),
235
+ }));
236
+
237
+ const metricRegistry = options.metricRegistry ?? createServerMetricRegistry();
238
+ metricRegistry.set(SERVER_METRIC_NAMES.buildInfo, 1, { version });
239
+
240
+ // IP-15: agent / workflow run completions move graphorin_agent_runs_total +
241
+ // graphorin_agent_run_duration_seconds. The tracker stays metric-agnostic -
242
+ // it fires a terminal callback the server turns into samples.
243
+ const runs =
244
+ options.runs ??
245
+ new RunStateTracker({
246
+ now,
247
+ onTerminal: (info) => {
248
+ metricRegistry.inc(SERVER_METRIC_NAMES.agentRunsTotal, { status: info.status });
249
+ if (info.durationMs !== undefined) {
250
+ metricRegistry.observe(SERVER_METRIC_NAMES.agentRunDuration, info.durationMs / 1000);
251
+ }
252
+ },
253
+ });
254
+ const agents = options.agents ?? new AgentRegistry();
255
+ const workflows = options.workflows ?? new WorkflowRegistry();
256
+
257
+ const app = new Hono<{ Variables: ServerVariables }>();
258
+
259
+ // Request-state / CORS / CSRF / rate-limit - see `app-middleware.ts`.
260
+ attachGlobalMiddleware(app, config, now);
261
+
262
+ const { triggersDaemon, consolidatorDaemon, workflowTimerDaemon } = buildDaemons(options);
263
+
264
+ const ws = buildWsLayer(app, config, now);
265
+
266
+ const lifecycle = createLifecycle({
267
+ options,
268
+ config,
269
+ store,
270
+ app,
271
+ metricRegistry,
272
+ runs,
273
+ agents,
274
+ workflows,
275
+ version,
276
+ startedAt,
277
+ now,
278
+ ws,
279
+ triggersDaemon,
280
+ consolidatorDaemon,
281
+ workflowTimerDaemon,
282
+ });
283
+
284
+ const handle: GraphorinServer = {
285
+ version,
286
+ config,
287
+ app,
288
+ agents,
289
+ workflows,
290
+ runs,
291
+ metrics: metricRegistry,
292
+ get wsDispatcher() {
293
+ return lifecycle.wsDispatcher;
294
+ },
295
+ get wsTickets() {
296
+ return lifecycle.wsTickets;
297
+ },
298
+ get triggers() {
299
+ return triggersDaemon;
300
+ },
301
+ get consolidator() {
302
+ return consolidatorDaemon;
303
+ },
304
+ get workflowTimers() {
305
+ return workflowTimerDaemon;
306
+ },
307
+ get listeningOn() {
308
+ return lifecycle.listeningOn;
309
+ },
310
+ start: lifecycle.start,
311
+ stop: lifecycle.stop,
312
+ };
313
+ return handle;
314
+ }
315
+
316
+ void process;
317
+ void ConfigInvalidError;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * IP-21: bridge the delivery-commentary sanitizer's audit decisions into the
3
+ * tamper-evident audit log. `createWsDispatcher` ran the sanitizer without a
4
+ * `sink`, so every documented sanitization decision - what was wrapped or
5
+ * stripped on the wire, with before/after digests and the matched-pattern
6
+ * bucket - was silently dropped instead of landing in the audit chain.
7
+ *
8
+ * The WS dispatcher is constructed before the audit DB is unsealed (the DB
9
+ * opens during `start()`), so the server hands the dispatcher a
10
+ * {@link LateBoundCommentarySink} and installs the real audit-writing target
11
+ * via {@link bridgeCommentaryToAudit} once the DB exists. Mirrors the
12
+ * `bridgeAuthToAudit` / `bridgeSecretsToAudit` pattern in `@graphorin/security`.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ import { type AuditDb, type AuditEntryInput, appendAudit } from '@graphorin/security/audit';
18
+
19
+ import type { DeliveryCommentaryDecision, DeliveryCommentarySink } from './types.js';
20
+
21
+ /**
22
+ * Audit action recorded for a delivery-commentary sanitization decision.
23
+ *
24
+ * @stable
25
+ */
26
+ export const COMMENTARY_AUDIT_ACTION = 'delivery:commentary:sanitized';
27
+
28
+ /**
29
+ * Translate a sanitizer decision into an audit entry. The digests + matched
30
+ * pattern bucket land in `metadata`; raw payloads never do (the sanitizer only
31
+ * ever exposes SHA-256s of the before/after bodies).
32
+ */
33
+ export function commentaryDecisionToAuditInput(
34
+ decision: DeliveryCommentaryDecision,
35
+ ): AuditEntryInput {
36
+ return {
37
+ actor: { kind: 'system', id: 'graphorin/server' },
38
+ action: COMMENTARY_AUDIT_ACTION,
39
+ target: `${decision.transport}:${decision.eventType}`,
40
+ decision: 'success',
41
+ metadata: {
42
+ policy: decision.policy,
43
+ applied: decision.applied,
44
+ boundary: decision.boundary,
45
+ reasons: decision.reasons,
46
+ ...(decision.matchedPattern !== undefined ? { matchedPattern: decision.matchedPattern } : {}),
47
+ sha256OfBefore: decision.sha256OfBefore,
48
+ sha256OfAfter: decision.sha256OfAfter,
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * A commentary sink that also exposes a `drain()` so callers (and tests) can
55
+ * await any in-flight audit writes.
56
+ *
57
+ * @stable
58
+ */
59
+ export interface CommentaryAuditSink extends DeliveryCommentarySink {
60
+ /** Resolve once every queued audit write has settled. */
61
+ readonly drain: () => Promise<void>;
62
+ }
63
+
64
+ /**
65
+ * Build a commentary sink that appends each sanitization decision to the audit
66
+ * log. Writes serialise through `appendAudit` so concurrent decisions never
67
+ * race on `seq`; a failed write is isolated from the wire - `onWriteError`
68
+ * (default: a console warning) runs instead of throwing.
69
+ *
70
+ * @stable
71
+ */
72
+ export function bridgeCommentaryToAudit(
73
+ db: AuditDb,
74
+ onWriteError: (
75
+ decision: DeliveryCommentaryDecision,
76
+ error: unknown,
77
+ ) => void = defaultOnWriteError,
78
+ ): CommentaryAuditSink {
79
+ let tail: Promise<unknown> = Promise.resolve();
80
+ return {
81
+ onDecision(decision) {
82
+ const input = commentaryDecisionToAuditInput(decision);
83
+ tail = tail
84
+ .then(() => appendAudit(db, input))
85
+ .catch((error) => onWriteError(decision, error));
86
+ },
87
+ drain: async () => {
88
+ await tail;
89
+ },
90
+ };
91
+ }
92
+
93
+ function defaultOnWriteError(_decision: DeliveryCommentaryDecision, error: unknown): void {
94
+ console.warn(
95
+ `[graphorin/server] WARN: failed to write a delivery-commentary audit entry: ${
96
+ error instanceof Error ? error.message : String(error)
97
+ }`,
98
+ );
99
+ }
100
+
101
+ /**
102
+ * A {@link DeliveryCommentarySink} whose real target is installed later. The WS
103
+ * dispatcher is created before the audit DB opens (IP-21); the server hands it
104
+ * this forwarding sink and calls {@link LateBoundCommentarySink.bind} once the
105
+ * audit-writing sink exists. Decisions emitted before binding are dropped - the
106
+ * dispatcher only sanitizes once it is live (after `start()`, by which point
107
+ * the audit DB, if configured, has opened and bound).
108
+ *
109
+ * @stable
110
+ */
111
+ export interface LateBoundCommentarySink extends DeliveryCommentarySink {
112
+ /** Install the real sink. Replaces any previously-bound target. */
113
+ bind(target: DeliveryCommentarySink): void;
114
+ }
115
+
116
+ /**
117
+ * @stable
118
+ */
119
+ export function createLateBoundCommentarySink(): LateBoundCommentarySink {
120
+ let target: DeliveryCommentarySink | undefined;
121
+ return {
122
+ bind(next) {
123
+ target = next;
124
+ },
125
+ onDecision(decision) {
126
+ // Contract: a sink never throws into the wire. The sanitizer also guards,
127
+ // but be defensive in case a bound target misbehaves.
128
+ try {
129
+ target?.onDecision(decision);
130
+ } catch {
131
+ // swallow
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Default pattern catalogue for the delivery-layer commentary-phase
3
+ * trace sanitization applied by the WebSocket dispatcher (`@graphorin/server/ws`)
4
+ * and the SSE event-emission boundary (`@graphorin/server/sse`).
5
+ *
6
+ * The catalogue is structurally identical to the session-output
7
+ * sanitization catalogue exposed by `@graphorin/sessions/commentary`;
8
+ * they live in two packages so the server's delivery layer does not
9
+ * pull a hard dependency on the session module while still providing
10
+ * defense-in-depth across the storage-write boundary (Phase 11) and
11
+ * the wire-emission boundary (this module). Deployments that want a
12
+ * single source of truth across both layers can pass the sessions
13
+ * catalogue through {@link import('./types.js').DeliveryCommentaryConfig}.patterns.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ import type { DeliveryCommentaryPattern } from './types.js';
19
+
20
+ /**
21
+ * The framework-shipped catalogue. Snapshot bytes-equal across the
22
+ * `ws` / `sse` / `rest` transports; idempotent on a single payload
23
+ * (the wrap envelope itself is not matched by any pattern, so a
24
+ * second pass over a previously-sanitized payload is a no-op).
25
+ *
26
+ * @stable
27
+ */
28
+ export const DEFAULT_DELIVERY_COMMENTARY_PATTERNS: ReadonlyArray<DeliveryCommentaryPattern> =
29
+ Object.freeze([
30
+ Object.freeze({
31
+ reason: 'tool.call.start-payload-signature',
32
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.start"[\s\S]*?"toolName"\s*:\s*"[^"]+"[\s\S]*?\}/g,
33
+ description:
34
+ 'JSON-encoded `tool.call.start` event with a `toolName` field - leaks the internal tool dispatch.',
35
+ }),
36
+ Object.freeze({
37
+ reason: 'tool.call.delta-payload-signature',
38
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.delta"[\s\S]*?"argsDelta"[\s\S]*?\}/g,
39
+ description: 'JSON-encoded `tool.call.delta` event with streaming `argsDelta`.',
40
+ }),
41
+ Object.freeze({
42
+ reason: 'tool.call.end-payload-signature',
43
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.end"[\s\S]*?"finalArgs"[\s\S]*?\}/g,
44
+ description: 'JSON-encoded `tool.call.end` event carrying the final args payload.',
45
+ }),
46
+ Object.freeze({
47
+ reason: 'tool.execute.end-payload-signature',
48
+ regex: /\{\s*"type"\s*:\s*"tool\.execute\.end"[\s\S]*?"result"\s*:\s*[\s\S]*?\}/g,
49
+ description: 'JSON-encoded `tool.execute.end` event with the raw `result` payload.',
50
+ }),
51
+ Object.freeze({
52
+ reason: 'agent.fanout-event-signature',
53
+ regex: /\{\s*"type"\s*:\s*"agent\.fanout\.(?:spawned|merged)"[\s\S]*?\}/g,
54
+ description: 'JSON-encoded `agent.fanout.spawned` / `agent.fanout.merged` event.',
55
+ }),
56
+ Object.freeze({
57
+ reason: 'context.compacted-event-signature',
58
+ regex: /\{\s*"type"\s*:\s*"context\.compacted"[\s\S]*?"originalTokens"[\s\S]*?\}/g,
59
+ description: 'JSON-encoded `context.compacted` event with token deltas.',
60
+ }),
61
+ Object.freeze({
62
+ reason: 'agent.model.fellback-event-signature',
63
+ regex: /\{\s*"type"\s*:\s*"agent\.model\.fellback"[\s\S]*?"fromModel"[\s\S]*?\}/g,
64
+ description: 'JSON-encoded `agent.model.fellback` event with fallback chain detail.',
65
+ }),
66
+ ]);
67
+
68
+ /**
69
+ * Default whitelist of `event.type` strings the dispatcher walks
70
+ * through the sanitizer. Extension is opt-in via
71
+ * `DeliveryCommentaryConfig.applyToEvents`.
72
+ *
73
+ * @stable
74
+ */
75
+ export const DEFAULT_APPLY_TO_EVENTS: ReadonlyArray<string> = Object.freeze([
76
+ 'tool.execute.end',
77
+ 'tool.execute.error',
78
+ 'text.delta',
79
+ ]);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Delivery-layer commentary-phase trace sanitization for
3
+ * `@graphorin/server`.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ export {
9
+ bridgeCommentaryToAudit,
10
+ COMMENTARY_AUDIT_ACTION,
11
+ type CommentaryAuditSink,
12
+ commentaryDecisionToAuditInput,
13
+ createLateBoundCommentarySink,
14
+ type LateBoundCommentarySink,
15
+ } from './audit-bridge.js';
16
+ export {
17
+ DEFAULT_APPLY_TO_EVENTS,
18
+ DEFAULT_DELIVERY_COMMENTARY_PATTERNS,
19
+ } from './built-in-patterns.js';
20
+ export {
21
+ createDeliveryCommentarySanitizer,
22
+ type DeliveryCommentarySanitizer,
23
+ } from './sanitizer.js';
24
+ export type {
25
+ DeliveryCommentaryConfig,
26
+ DeliveryCommentaryDecision,
27
+ DeliveryCommentaryPattern,
28
+ DeliveryCommentaryPolicy,
29
+ DeliveryCommentaryReason,
30
+ DeliveryCommentarySink,
31
+ DeliveryCommentaryTransport,
32
+ } from './types.js';