@graphorin/server 0.5.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 (204) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/dist/app.d.ts +174 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +684 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/commentary/audit-bridge.d.ts +57 -0
  9. package/dist/commentary/audit-bridge.d.ts.map +1 -0
  10. package/dist/commentary/audit-bridge.js +92 -0
  11. package/dist/commentary/audit-bridge.js.map +1 -0
  12. package/dist/commentary/built-in-patterns.d.ts +24 -0
  13. package/dist/commentary/built-in-patterns.d.ts.map +1 -0
  14. package/dist/commentary/built-in-patterns.js +62 -0
  15. package/dist/commentary/built-in-patterns.js.map +1 -0
  16. package/dist/commentary/index.d.ts +5 -0
  17. package/dist/commentary/index.js +5 -0
  18. package/dist/commentary/sanitizer.d.ts +37 -0
  19. package/dist/commentary/sanitizer.d.ts.map +1 -0
  20. package/dist/commentary/sanitizer.js +182 -0
  21. package/dist/commentary/sanitizer.js.map +1 -0
  22. package/dist/commentary/types.d.ts +120 -0
  23. package/dist/commentary/types.d.ts.map +1 -0
  24. package/dist/config.d.ts +760 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +217 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consolidator/daemon.d.ts +88 -0
  29. package/dist/consolidator/daemon.d.ts.map +1 -0
  30. package/dist/consolidator/daemon.js +54 -0
  31. package/dist/consolidator/daemon.js.map +1 -0
  32. package/dist/consolidator/index.d.ts +2 -0
  33. package/dist/consolidator/index.js +3 -0
  34. package/dist/errors/index.d.ts +104 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +131 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/health/checks.d.ts +120 -0
  39. package/dist/health/checks.d.ts.map +1 -0
  40. package/dist/health/checks.js +127 -0
  41. package/dist/health/checks.js.map +1 -0
  42. package/dist/health/index.d.ts +3 -0
  43. package/dist/health/index.js +4 -0
  44. package/dist/health/routes.d.ts +66 -0
  45. package/dist/health/routes.d.ts.map +1 -0
  46. package/dist/health/routes.js +96 -0
  47. package/dist/health/routes.js.map +1 -0
  48. package/dist/index.d.ts +88 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +86 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/context.d.ts +66 -0
  53. package/dist/internal/context.d.ts.map +1 -0
  54. package/dist/internal/ids.js +21 -0
  55. package/dist/internal/ids.js.map +1 -0
  56. package/dist/internal/json.js +46 -0
  57. package/dist/internal/json.js.map +1 -0
  58. package/dist/lifecycle/hooks.d.ts +57 -0
  59. package/dist/lifecycle/hooks.d.ts.map +1 -0
  60. package/dist/lifecycle/index.d.ts +2 -0
  61. package/dist/lifecycle/index.js +3 -0
  62. package/dist/lifecycle/pre-bind.d.ts +38 -0
  63. package/dist/lifecycle/pre-bind.d.ts.map +1 -0
  64. package/dist/lifecycle/pre-bind.js +62 -0
  65. package/dist/lifecycle/pre-bind.js.map +1 -0
  66. package/dist/metrics/catalog.d.ts +34 -0
  67. package/dist/metrics/catalog.d.ts.map +1 -0
  68. package/dist/metrics/catalog.js +61 -0
  69. package/dist/metrics/catalog.js.map +1 -0
  70. package/dist/metrics/index.d.ts +3 -0
  71. package/dist/metrics/index.js +4 -0
  72. package/dist/metrics/registry.d.ts +63 -0
  73. package/dist/metrics/registry.d.ts.map +1 -0
  74. package/dist/metrics/registry.js +222 -0
  75. package/dist/metrics/registry.js.map +1 -0
  76. package/dist/middleware/audit.d.ts +47 -0
  77. package/dist/middleware/audit.d.ts.map +1 -0
  78. package/dist/middleware/audit.js +71 -0
  79. package/dist/middleware/audit.js.map +1 -0
  80. package/dist/middleware/auth.d.ts +50 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +164 -0
  83. package/dist/middleware/auth.js.map +1 -0
  84. package/dist/middleware/cors.d.ts +15 -0
  85. package/dist/middleware/cors.d.ts.map +1 -0
  86. package/dist/middleware/cors.js +45 -0
  87. package/dist/middleware/cors.js.map +1 -0
  88. package/dist/middleware/csrf.d.ts +15 -0
  89. package/dist/middleware/csrf.d.ts.map +1 -0
  90. package/dist/middleware/csrf.js +77 -0
  91. package/dist/middleware/csrf.js.map +1 -0
  92. package/dist/middleware/idempotency.d.ts +41 -0
  93. package/dist/middleware/idempotency.d.ts.map +1 -0
  94. package/dist/middleware/idempotency.js +198 -0
  95. package/dist/middleware/idempotency.js.map +1 -0
  96. package/dist/middleware/index.d.ts +9 -0
  97. package/dist/middleware/index.js +10 -0
  98. package/dist/middleware/rate-limit.d.ts +17 -0
  99. package/dist/middleware/rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/rate-limit.js +47 -0
  101. package/dist/middleware/rate-limit.js.map +1 -0
  102. package/dist/middleware/request-state.d.ts +27 -0
  103. package/dist/middleware/request-state.d.ts.map +1 -0
  104. package/dist/middleware/request-state.js +42 -0
  105. package/dist/middleware/request-state.js.map +1 -0
  106. package/dist/middleware/scope.d.ts +24 -0
  107. package/dist/middleware/scope.d.ts.map +1 -0
  108. package/dist/middleware/scope.js +50 -0
  109. package/dist/middleware/scope.js.map +1 -0
  110. package/dist/registry/index.d.ts +135 -0
  111. package/dist/registry/index.d.ts.map +1 -0
  112. package/dist/registry/index.js +96 -0
  113. package/dist/registry/index.js.map +1 -0
  114. package/dist/replay/index.d.ts +2 -0
  115. package/dist/replay/index.js +3 -0
  116. package/dist/replay/routes.d.ts +46 -0
  117. package/dist/replay/routes.d.ts.map +1 -0
  118. package/dist/replay/routes.js +189 -0
  119. package/dist/replay/routes.js.map +1 -0
  120. package/dist/routes/agents.d.ts +41 -0
  121. package/dist/routes/agents.d.ts.map +1 -0
  122. package/dist/routes/agents.js +213 -0
  123. package/dist/routes/agents.js.map +1 -0
  124. package/dist/routes/audit.d.ts +57 -0
  125. package/dist/routes/audit.d.ts.map +1 -0
  126. package/dist/routes/audit.js +116 -0
  127. package/dist/routes/audit.js.map +1 -0
  128. package/dist/routes/auth.d.ts +26 -0
  129. package/dist/routes/auth.d.ts.map +1 -0
  130. package/dist/routes/auth.js +54 -0
  131. package/dist/routes/auth.js.map +1 -0
  132. package/dist/routes/health.d.ts +22 -0
  133. package/dist/routes/health.d.ts.map +1 -0
  134. package/dist/routes/health.js +33 -0
  135. package/dist/routes/health.js.map +1 -0
  136. package/dist/routes/index.d.ts +10 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/mcp.d.ts +32 -0
  139. package/dist/routes/mcp.d.ts.map +1 -0
  140. package/dist/routes/mcp.js +61 -0
  141. package/dist/routes/mcp.js.map +1 -0
  142. package/dist/routes/memory.d.ts +141 -0
  143. package/dist/routes/memory.d.ts.map +1 -0
  144. package/dist/routes/memory.js +102 -0
  145. package/dist/routes/memory.js.map +1 -0
  146. package/dist/routes/sessions.d.ts +54 -0
  147. package/dist/routes/sessions.d.ts.map +1 -0
  148. package/dist/routes/sessions.js +135 -0
  149. package/dist/routes/sessions.js.map +1 -0
  150. package/dist/routes/skills.d.ts +34 -0
  151. package/dist/routes/skills.d.ts.map +1 -0
  152. package/dist/routes/skills.js +54 -0
  153. package/dist/routes/skills.js.map +1 -0
  154. package/dist/routes/tokens.d.ts +25 -0
  155. package/dist/routes/tokens.d.ts.map +1 -0
  156. package/dist/routes/tokens.js +87 -0
  157. package/dist/routes/tokens.js.map +1 -0
  158. package/dist/routes/workflows.d.ts +27 -0
  159. package/dist/routes/workflows.d.ts.map +1 -0
  160. package/dist/routes/workflows.js +220 -0
  161. package/dist/routes/workflows.js.map +1 -0
  162. package/dist/runtime/run-state.d.ts +158 -0
  163. package/dist/runtime/run-state.d.ts.map +1 -0
  164. package/dist/runtime/run-state.js +182 -0
  165. package/dist/runtime/run-state.js.map +1 -0
  166. package/dist/sse/events.d.ts +44 -0
  167. package/dist/sse/events.d.ts.map +1 -0
  168. package/dist/sse/events.js +200 -0
  169. package/dist/sse/events.js.map +1 -0
  170. package/dist/sse/index.d.ts +2 -0
  171. package/dist/sse/index.js +3 -0
  172. package/dist/triggers/daemon.d.ts +74 -0
  173. package/dist/triggers/daemon.d.ts.map +1 -0
  174. package/dist/triggers/daemon.js +114 -0
  175. package/dist/triggers/daemon.js.map +1 -0
  176. package/dist/triggers/index.d.ts +3 -0
  177. package/dist/triggers/index.js +4 -0
  178. package/dist/triggers/routes.d.ts +21 -0
  179. package/dist/triggers/routes.d.ts.map +1 -0
  180. package/dist/triggers/routes.js +117 -0
  181. package/dist/triggers/routes.js.map +1 -0
  182. package/dist/ws/dispatcher.d.ts +169 -0
  183. package/dist/ws/dispatcher.d.ts.map +1 -0
  184. package/dist/ws/dispatcher.js +303 -0
  185. package/dist/ws/dispatcher.js.map +1 -0
  186. package/dist/ws/index.d.ts +6 -0
  187. package/dist/ws/index.js +7 -0
  188. package/dist/ws/replay-buffer.d.ts +47 -0
  189. package/dist/ws/replay-buffer.d.ts.map +1 -0
  190. package/dist/ws/replay-buffer.js +88 -0
  191. package/dist/ws/replay-buffer.js.map +1 -0
  192. package/dist/ws/subjects.d.ts +71 -0
  193. package/dist/ws/subjects.d.ts.map +1 -0
  194. package/dist/ws/subjects.js +112 -0
  195. package/dist/ws/subjects.js.map +1 -0
  196. package/dist/ws/ticket.d.ts +74 -0
  197. package/dist/ws/ticket.d.ts.map +1 -0
  198. package/dist/ws/ticket.js +112 -0
  199. package/dist/ws/ticket.js.map +1 -0
  200. package/dist/ws/upgrade.d.ts +63 -0
  201. package/dist/ws/upgrade.d.ts.map +1 -0
  202. package/dist/ws/upgrade.js +324 -0
  203. package/dist/ws/upgrade.js.map +1 -0
  204. package/package.json +156 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","names":["config: ServerConfigSpec","storeEncryption:\n | { enabled: true; cipher?: never; passphraseResolver: () => Promise<string> }\n | undefined","serverInstance: ServerType | undefined","listening: { readonly host: string; readonly port: number } | undefined","stopRunPruning: (() => void) | undefined","auditDb: AuditDb | undefined","preBind: Awaited<ReturnType<typeof runPreBind>> | undefined","verifier: TokenVerifier | undefined","pepperHandle: import('@graphorin/security').SecretValue | undefined","triggersDaemon: TriggersDaemon | undefined","consolidatorDaemon: ConsolidatorDaemon | undefined","dispatcher: WsDispatcher | undefined","tickets: WsTicketStore | undefined","wsAdapter: ReturnType<typeof createNodeWebSocket> | undefined","commentaryAuditSink: LateBoundCommentarySink | undefined","out: {\n -readonly [K in keyof HealthCheckOptions]?: HealthCheckOptions[K];\n }"],"sources":["../src/app.ts"],"sourcesContent":["/**\n * `createServer({...})` — the single programmatic entry point.\n *\n * The factory returns a {@link GraphorinServer} handle the operator\n * controls via `start()` / `stop()`. The same handle is consumed by\n * the `graphorin start` CLI binary in `@graphorin/cli`.\n *\n * Phase 14a covers the full HTTP surface, the lifecycle plumbing,\n * the Hono app composition, and the auth / scope / idempotency /\n * audit middleware stack. Phase 14b/c attach the WebSocket layer +\n * triggers daemon + observability endpoints to the same handle.\n *\n * @packageDocumentation\n */\n\nimport type { AddressInfo } from 'node:net';\nimport process from 'node:process';\nimport { negotiateSubprotocol, SUBPROTOCOL_NAME } from '@graphorin/protocol';\nimport { applyProcessHardening, generatePepper, TokenVerifier } from '@graphorin/security';\nimport { type AuditDb, openAuditDb, registerAuditDbBinding } from '@graphorin/security/audit';\nimport {\n createSqliteStore,\n type GraphorinSqliteStore,\n loadCipherDriver,\n readWalSize,\n} from '@graphorin/store-sqlite';\nimport { type ServerType, serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { Hono } from 'hono';\nimport {\n bridgeCommentaryToAudit,\n createLateBoundCommentarySink,\n type LateBoundCommentarySink,\n} from './commentary/index.js';\nimport { parseServerConfig, type ServerConfigInput, type ServerConfigSpec } from './config.js';\nimport type { ConsolidatorDaemon, ConsolidatorLike } from './consolidator/daemon.js';\nimport { createConsolidatorDaemon } from './consolidator/daemon.js';\nimport {\n ConfigInvalidError,\n LifecycleDoubleStartError,\n LifecycleNotStartedError,\n ShutdownTimeoutError,\n} from './errors/index.js';\nimport {\n createExtendedHealthRoutes,\n createMetricsRoutes,\n createSecretsHealthRoutes,\n type HealthCheckOptions,\n} from './health/index.js';\nimport type { ServerVariables } from './internal/context.js';\nimport { type LifecycleHooks, type OnErrorContext, runPreBind } from './lifecycle/index.js';\nimport { createServerMetricRegistry, SERVER_METRIC_NAMES } from './metrics/catalog.js';\nimport type { MetricRegistry } from './metrics/registry.js';\nimport {\n createAnonymousAuthMiddleware,\n createAuditMiddleware,\n createAuthMiddleware,\n createCorsMiddleware,\n createCsrfMiddleware,\n createIdempotencyMiddleware,\n createRateLimitMiddleware,\n createRequestStateMiddleware,\n} from './middleware/index.js';\nimport { AgentRegistry, WorkflowRegistry } from './registry/index.js';\nimport { createReplayRoutes, type ReplayApi } from './replay/index.js';\nimport {\n type AuditApi,\n createAgentRoutes,\n createAuditRoutes,\n createAuthRoutes,\n createMcpRoutes,\n createMemoryRoutes,\n createRunRoutes,\n createSessionRoutes,\n createSkillsRoutes,\n createTokensRoutes,\n createWorkflowRoutes,\n type McpApi,\n type MemoryApi,\n type SessionApi,\n type SkillsApi,\n} from './routes/index.js';\nimport { RunStateTracker, scheduleRunPruning } from './runtime/run-state.js';\nimport { createSseRoutes } from './sse/index.js';\nimport type { TriggersDaemon } from './triggers/daemon.js';\nimport { createTriggersDaemon } from './triggers/daemon.js';\nimport { createTriggersRoutes } from './triggers/routes.js';\nimport {\n createWsDispatcher,\n createWsTicketStore,\n type WsDispatcher,\n type WsTicketStore,\n} from './ws/index.js';\nimport { createWsUpgradeEvents } from './ws/upgrade.js';\n\n/**\n * Pre-built audit-db binding shipped from `@graphorin/store-sqlite`.\n * Registered exactly once per process so {@link openAuditDb} can find\n * a default binding without forcing operators to wire it manually.\n *\n * Exported as {@link ensureStoreAuditBinding} so the CLI (Phase 15\n * `graphorin audit verify | prune | export`) can reach into the same\n * binding without booting the HTTP listener.\n *\n * @stable\n */\nlet storeAuditBindingRegistered = false;\nexport function ensureStoreAuditBinding(): void {\n if (storeAuditBindingRegistered) return;\n registerAuditDbBinding(\n {\n id: 'better-sqlite3-multiple-ciphers',\n description: 'Default audit-db binding shipped by @graphorin/store-sqlite.',\n open: async (opts) => {\n const driver = await loadCipherDriver();\n const passphrase = await opts.passphrase.use((value) => value);\n const Db = driver as unknown as new (\n path: string,\n ) => {\n pragma(s: string): unknown;\n prepare(sql: string): {\n run(...args: unknown[]): unknown;\n get(...args: unknown[]): unknown;\n all(...args: unknown[]): unknown;\n iterate(...args: unknown[]): IterableIterator<unknown>;\n };\n exec(sql: string): unknown;\n close(): void;\n open: boolean;\n };\n const db = new Db(opts.path);\n db.pragma(`key = '${passphrase.replace(/'/g, \"''\")}'`);\n db.pragma('journal_mode = WAL');\n db.pragma('synchronous = NORMAL');\n db.pragma('busy_timeout = 5000');\n db.pragma('foreign_keys = ON');\n db.exec(\n `CREATE TABLE IF NOT EXISTS audit_log (\n seq INTEGER PRIMARY KEY,\n ts INTEGER NOT NULL,\n actor_json TEXT NOT NULL,\n action TEXT NOT NULL,\n target TEXT NOT NULL,\n decision TEXT NOT NULL,\n context_json TEXT,\n metadata_json TEXT,\n prev_hash TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE\n ) WITHOUT ROWID;`,\n );\n return {\n binding: 'better-sqlite3-multiple-ciphers',\n path: opts.path,\n async insert(entry) {\n db.prepare(\n `INSERT INTO audit_log (\n seq, ts, actor_json, action, target, decision,\n context_json, metadata_json, prev_hash, hash\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n entry.seq,\n entry.ts,\n JSON.stringify(entry.actor),\n entry.action,\n entry.target,\n entry.decision,\n entry.context !== undefined ? JSON.stringify(entry.context) : null,\n entry.metadata !== undefined ? JSON.stringify(entry.metadata) : null,\n entry.prevHash,\n entry.hash,\n );\n return entry;\n },\n async latest() {\n const row = db.prepare('SELECT * FROM audit_log ORDER BY seq DESC LIMIT 1').get() as\n | undefined\n | {\n seq: number;\n ts: number;\n actor_json: string;\n action: string;\n target: string;\n decision: string;\n context_json: string | null;\n metadata_json: string | null;\n prev_hash: string;\n hash: string;\n };\n if (row === undefined) return undefined;\n return rowToEntry(row);\n },\n async *iterate(bounds) {\n const lo = bounds?.fromSeq ?? 1;\n const hi = bounds?.toSeq ?? Number.MAX_SAFE_INTEGER;\n const iter = db\n .prepare('SELECT * FROM audit_log WHERE seq BETWEEN ? AND ? ORDER BY seq ASC')\n .iterate(lo, hi);\n for (const row of iter) yield rowToEntry(row as Parameters<typeof rowToEntry>[0]);\n },\n async count() {\n const row = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get() as { n: number };\n return row.n;\n },\n async deleteUpTo(threshold) {\n const before = db\n .prepare('SELECT COUNT(*) AS n FROM audit_log WHERE seq <= ?')\n .get(threshold) as { n: number };\n db.prepare('DELETE FROM audit_log WHERE seq <= ?').run(threshold);\n return before.n;\n },\n async replaceEntry(entry) {\n db.prepare(`UPDATE audit_log SET prev_hash = ?, hash = ? WHERE seq = ?`).run(\n entry.prevHash,\n entry.hash,\n entry.seq,\n );\n },\n async close() {\n if (db.open) db.close();\n },\n };\n },\n },\n { setAsDefault: true },\n );\n storeAuditBindingRegistered = true;\n}\n\nfunction rowToEntry(row: {\n seq: number;\n ts: number;\n actor_json: string;\n action: string;\n target: string;\n decision: string;\n context_json: string | null;\n metadata_json: string | null;\n prev_hash: string;\n hash: string;\n}) {\n return {\n seq: row.seq,\n ts: row.ts,\n actor: JSON.parse(row.actor_json),\n action: row.action,\n target: row.target,\n decision: row.decision as 'success' | 'denied' | 'error' | 'not-found',\n ...(row.context_json !== null ? { context: JSON.parse(row.context_json) } : {}),\n ...(row.metadata_json !== null ? { metadata: JSON.parse(row.metadata_json) } : {}),\n prevHash: row.prev_hash,\n hash: row.hash,\n };\n}\n\n/**\n * Public surface returned by {@link createServer}.\n *\n * @stable\n */\nexport interface GraphorinServer {\n readonly version: string;\n readonly config: ServerConfigSpec;\n readonly app: Hono<{ Variables: ServerVariables }>;\n readonly agents: AgentRegistry;\n readonly workflows: WorkflowRegistry;\n readonly runs: RunStateTracker;\n readonly listeningOn: { readonly host: string; readonly port: number } | undefined;\n /**\n * Optional WebSocket dispatcher exposed when `server.ws.enabled =\n * true`. Route handlers + the agent / workflow runtimes call\n * `dispatcher.emit(subject, event)` to fan out events to subscribed\n * clients. Returns `undefined` when WS is disabled.\n */\n readonly wsDispatcher: WsDispatcher | undefined;\n /**\n * Optional WebSocket ticket store exposed when `server.ws.enabled\n * = true`. Surfaced primarily for tests; the\n * `POST /v1/session/ws-ticket` route uses it transparently.\n */\n readonly wsTickets: WsTicketStore | undefined;\n /**\n * Optional triggers daemon — populated when the operator wired a\n * scheduler (or an in-process trigger surface) at construction\n * time. Phase 14c integration.\n */\n readonly triggers: TriggersDaemon | undefined;\n /**\n * Optional consolidator daemon — populated when the operator\n * supplied a `Consolidator` instance via `createServer({\n * consolidator })`. Phase 14c integration.\n */\n readonly consolidator: ConsolidatorDaemon | undefined;\n /**\n * Phase 14c Prometheus registry. Always present; sample updates\n * are observable via `metrics.snapshot()`.\n */\n readonly metrics: MetricRegistry;\n start(): Promise<{ readonly host: string; readonly port: number }>;\n stop(options?: { readonly force?: boolean }): Promise<void>;\n}\n\n/**\n * Discriminated union accepted by `CreateServerOptions.triggers`. A\n * caller may either supply a fully-built daemon (e.g. constructed\n * around a custom `Scheduler`) or just the underlying scheduler — the\n * server wraps it with {@link createTriggersDaemon} automatically.\n *\n * @stable\n */\nexport type TriggersDaemonInput =\n | { readonly daemon: TriggersDaemon }\n | { readonly scheduler: import('@graphorin/triggers').Scheduler };\n\n/**\n * @stable\n */\nexport interface CreateServerOptions {\n /** Loaded `graphorin.config.ts` payload — see `defineConfig({...})`. */\n readonly config?: ServerConfigInput;\n /**\n * Optional pre-validated config. When supplied, `config` is ignored\n * and the schema validation step is skipped. Useful for tests + the\n * `graphorin migrate` CLI command which bypasses the listener.\n */\n readonly validatedConfig?: ServerConfigSpec;\n /** Pre-built SQLite store. Tests inject an in-memory store. */\n readonly store?: GraphorinSqliteStore;\n /** Optional pre-built tracker. Tests inject deterministic timing. */\n readonly runs?: RunStateTracker;\n /** Optional pre-built registries. */\n readonly agents?: AgentRegistry;\n readonly workflows?: WorkflowRegistry;\n /** Optional in-process domain adapters wired into REST routes. */\n readonly sessions?: SessionApi;\n readonly memory?: MemoryApi;\n readonly skills?: SkillsApi;\n readonly mcp?: McpApi;\n readonly audit?: AuditApi;\n /**\n * Optional consolidator surface (`@graphorin/memory`). Phase 14c\n * starts/stops the runtime alongside the server lifecycle and\n * surfaces its status through `/v1/health`.\n */\n readonly consolidator?: ConsolidatorLike;\n /**\n * Optional triggers daemon — pass an existing one (e.g. built\n * from `createScheduler`) or a triggers configuration the server\n * should wrap with the daemon adapter.\n */\n readonly triggers?: TriggersDaemonInput;\n /**\n * Optional replay API consumed by the scope-enforced replay\n * endpoints. Phase 14c.\n */\n readonly replay?: ReplayApi;\n /**\n * Optional probes that augment `/v1/health`. Provided by consumer\n * code (e.g. `embedder` provides `embedderLoaded`).\n */\n readonly healthProbes?: () => HealthCheckOptions | Promise<HealthCheckOptions>;\n /**\n * Optional Prometheus metric registry override. When omitted, the\n * server constructs the canonical registry from\n * {@link createServerMetricRegistry}.\n */\n readonly metricRegistry?: MetricRegistry;\n /** Lifecycle hook overrides. */\n readonly hooks?: LifecycleHooks;\n /** Wall-clock provider for tests. */\n readonly now?: () => number;\n /** Override the cipher peer probe. Tests inject a stub. */\n readonly probeCipherPeer?: () => Promise<void>;\n /** Override the package version reported on `/v1/health`. */\n readonly version?: string;\n /** Skip `applyProcessHardening` (tests). */\n readonly skipHardening?: boolean;\n /** Skip starting the actual listener (tests). */\n readonly skipListen?: boolean;\n}\n\n/**\n * @stable\n */\nexport const VERSION = '0.5.0';\n\n/**\n * Build a fully-wired Graphorin server. The returned handle is\n * inert until `start()` is awaited.\n *\n * @stable\n */\nexport async function createServer(options: CreateServerOptions = {}): Promise<GraphorinServer> {\n const config: ServerConfigSpec =\n options.validatedConfig ?? parseServerConfig(options.config ?? {});\n const now = options.now ?? Date.now;\n const startedAt = now();\n const version = options.version ?? VERSION;\n\n // IP-1: when encryption is configured, resolve the passphrase ref\n // BEFORE constructing the store and thread the encryption config\n // through — `graphorin init --encrypted` produced a config nothing\n // honoured, and a database encrypted via `storage encrypt` could not\n // be opened by the server at all.\n let storeEncryption:\n | { enabled: true; cipher?: never; passphraseResolver: () => Promise<string> }\n | undefined;\n if (options.store === undefined && config.storage.encryption.enabled) {\n if (config.storage.encryption.passphraseRef === undefined) {\n throw new Error(\n '[graphorin/server] storage.encryption.enabled is true but no passphraseRef is configured.',\n );\n }\n const { resolveSecret } = await import('@graphorin/security/secrets');\n const passphrase = await resolveSecret(config.storage.encryption.passphraseRef);\n storeEncryption = {\n enabled: true,\n passphraseResolver: async () => passphrase.use((v) => v),\n } as never;\n }\n const store =\n options.store ??\n (await createSqliteStore({\n path: config.storage.path,\n mode: config.storage.mode,\n ...(storeEncryption !== undefined\n ? {\n encryption: {\n enabled: true,\n ...(config.storage.encryption.cipher !== undefined\n ? { cipher: config.storage.encryption.cipher as never }\n : {}),\n passphraseResolver: storeEncryption.passphraseResolver,\n },\n }\n : {}),\n }));\n\n const metricRegistry = options.metricRegistry ?? createServerMetricRegistry();\n metricRegistry.set(SERVER_METRIC_NAMES.buildInfo, 1, { version });\n\n // IP-15: agent / workflow run completions move graphorin_agent_runs_total +\n // graphorin_agent_run_duration_seconds. The tracker stays metric-agnostic —\n // it fires a terminal callback the server turns into samples.\n const runs =\n options.runs ??\n new RunStateTracker({\n now,\n onTerminal: (info) => {\n metricRegistry.inc(SERVER_METRIC_NAMES.agentRunsTotal, { status: info.status });\n if (info.durationMs !== undefined) {\n metricRegistry.observe(SERVER_METRIC_NAMES.agentRunDuration, info.durationMs / 1000);\n }\n },\n });\n const agents = options.agents ?? new AgentRegistry();\n const workflows = options.workflows ?? new WorkflowRegistry();\n\n const app = new Hono<{ Variables: ServerVariables }>();\n\n // Global middleware in the documented order. Per-route scope +\n // idempotency layers are mounted by the route factories so the\n // composition stays declarative + auditable.\n app.use(\n '*',\n createRequestStateMiddleware({\n now,\n ...(config.server.trustProxy ? { trustProxy: true } : {}),\n }),\n );\n app.use('*', createCorsMiddleware(config.server.cors));\n app.use('*', createCsrfMiddleware(config.server.csrf));\n app.use('*', createRateLimitMiddleware(config.server.rateLimit, { now }));\n\n let serverInstance: ServerType | undefined;\n let listening: { readonly host: string; readonly port: number } | undefined;\n let started = false;\n let stopped = false;\n // IP-16: stops the periodic terminal-run prune sweep on shutdown.\n let stopRunPruning: (() => void) | undefined;\n let auditDb: AuditDb | undefined;\n let preBind: Awaited<ReturnType<typeof runPreBind>> | undefined;\n let verifier: TokenVerifier | undefined;\n let pepperHandle: import('@graphorin/security').SecretValue | undefined;\n\n let triggersDaemon: TriggersDaemon | undefined;\n if (options.triggers !== undefined) {\n if ('daemon' in options.triggers) {\n triggersDaemon = options.triggers.daemon;\n } else {\n triggersDaemon = createTriggersDaemon({ scheduler: options.triggers.scheduler });\n }\n }\n let consolidatorDaemon: ConsolidatorDaemon | undefined;\n if (options.consolidator !== undefined) {\n consolidatorDaemon = createConsolidatorDaemon({ consolidator: options.consolidator });\n }\n\n let dispatcher: WsDispatcher | undefined;\n let tickets: WsTicketStore | undefined;\n let wsAdapter: ReturnType<typeof createNodeWebSocket> | undefined;\n // IP-21: the dispatcher is built here, before the audit DB opens in start().\n // Hand it a late-bound commentary sink now and install the audit-writing\n // target once the DB exists — otherwise the sanitizer's documented decisions\n // (wrapped/stripped frames with before/after digests) are dropped.\n let commentaryAuditSink: LateBoundCommentarySink | undefined;\n if (config.server.ws.enabled) {\n commentaryAuditSink = createLateBoundCommentarySink();\n dispatcher = createWsDispatcher({\n commentary: {\n policy: config.server.ws.commentarySanitization.policy,\n applyToEvents: config.server.ws.commentarySanitization.applyToEvents,\n sink: commentaryAuditSink,\n },\n replayBuffer: {\n maxEvents: config.server.stream.replayBuffer.maxEvents,\n ttlMs: config.server.stream.replayBuffer.ttlSeconds * 1000,\n },\n perConnectionQueueLimit: config.server.stream.perConnectionQueueLimit,\n now,\n });\n tickets = createWsTicketStore({\n ttlMs: config.server.ws.ticketTtlMs,\n now,\n });\n wsAdapter = createNodeWebSocket({ app });\n // The WS server inside @hono/node-ws is created without any\n // subprotocol-negotiation policy; without one, the `ws` library\n // never echoes back `Sec-WebSocket-Protocol` and clients that\n // advertised a subprotocol close the connection immediately.\n // Mutate the options to install a Graphorin-aware policy that\n // selects `graphorin.protocol.v1` when the client offered it.\n const wssOptions = (\n wsAdapter.wss as unknown as {\n options: {\n handleProtocols?: (\n protocols: Set<string>,\n request: import('node:http').IncomingMessage,\n ) => string | false;\n };\n }\n ).options;\n wssOptions.handleProtocols = (protocols) => {\n const negotiated = negotiateSubprotocol(Array.from(protocols));\n if (negotiated !== null) return negotiated;\n // Browser ticket flow: the `WebSocket` constructor cannot set an\n // `Authorization` header, so the browser client offers two\n // subprotocol tokens — the canonical `graphorin.protocol.v1`\n // name plus a `ticket.<value>` token (see the wire contract in\n // `@graphorin/protocol`'s `subprotocol.ts`:\n // `SUBPROTOCOL_NAME` / `TICKET_SUBPROTOCOL_PREFIX` /\n // `parseTicketSubprotocol`). The server MUST echo back exactly\n // the canonical name (never the `ticket.*` token) so the\n // handshake's `Sec-WebSocket-Protocol` response stays valid; the\n // ticket value is consumed separately by `createWsUpgradeEvents`\n // via `parseTicketSubprotocol` and exchanged through the\n // single-use `WsTicketStore`.\n for (const candidate of protocols) {\n if (candidate === SUBPROTOCOL_NAME) return SUBPROTOCOL_NAME;\n }\n return false;\n };\n }\n\n const handle: GraphorinServer = {\n version,\n config,\n app,\n agents,\n workflows,\n runs,\n metrics: metricRegistry,\n get wsDispatcher() {\n return dispatcher;\n },\n get wsTickets() {\n return tickets;\n },\n get triggers() {\n return triggersDaemon;\n },\n get consolidator() {\n return consolidatorDaemon;\n },\n get listeningOn() {\n return listening;\n },\n async start(): Promise<{ readonly host: string; readonly port: number }> {\n if (started) throw new LifecycleDoubleStartError();\n started = true;\n try {\n if (options.hooks?.beforeStart !== undefined) {\n await options.hooks.beforeStart({ config });\n }\n if (options.skipHardening !== true && config.hardening.applyOnStart) {\n applyProcessHardening({\n refuseRoot: config.hardening.refuseRoot,\n umask: config.hardening.umask,\n });\n }\n preBind = await runPreBind({\n config,\n store,\n ...(options.probeCipherPeer !== undefined\n ? { probeCipherPeer: options.probeCipherPeer }\n : {}),\n });\n\n if (\n config.audit.enabled &&\n preBind.auditPassphrase !== undefined &&\n preBind.auditPath !== undefined\n ) {\n ensureStoreAuditBinding();\n auditDb = await openAuditDb({\n path: preBind.auditPath,\n passphrase: preBind.auditPassphrase,\n ...(config.audit.cipher !== undefined ? { cipher: config.audit.cipher } : {}),\n });\n // IP-21: now that the audit chain is open, route the WS dispatcher's\n // commentary-sanitizer decisions into it.\n commentaryAuditSink?.bind(bridgeCommentaryToAudit(auditDb));\n }\n\n if (config.auth.kind === 'token') {\n const pepper = preBind.pepper ?? (await fallbackPepper(options.skipHardening === true));\n pepperHandle = pepper;\n verifier = new TokenVerifier({\n tokenStore: store.authTokens,\n pepper,\n acceptPrefix: config.auth.tokenPrefix,\n acceptEnvironments: config.auth.tokenEnvironments,\n ...(config.auth.perIpFailureThreshold !== undefined\n ? { perIpFailureThreshold: config.auth.perIpFailureThreshold }\n : {}),\n ...(config.auth.perIpLockoutMs !== undefined\n ? { perIpLockoutMs: config.auth.perIpLockoutMs }\n : {}),\n now,\n });\n }\n\n mountRoutes(app, config, {\n version,\n startedAt,\n now,\n agents,\n workflows,\n runs,\n store,\n metricRegistry,\n ...(options.sessions !== undefined ? { sessions: options.sessions } : {}),\n ...(options.memory !== undefined ? { memory: options.memory } : {}),\n ...(options.skills !== undefined ? { skills: options.skills } : {}),\n ...(options.mcp !== undefined ? { mcp: options.mcp } : {}),\n ...(options.audit !== undefined ? { audit: options.audit } : {}),\n ...(options.replay !== undefined ? { replay: options.replay } : {}),\n ...(options.healthProbes !== undefined ? { healthProbes: options.healthProbes } : {}),\n ...(verifier !== undefined ? { verifier } : {}),\n ...(auditDb !== undefined ? { auditDb } : {}),\n ...(pepperHandle !== undefined ? { pepper: pepperHandle } : {}),\n ...(dispatcher !== undefined ? { wsDispatcher: dispatcher } : {}),\n ...(tickets !== undefined ? { wsTickets: tickets } : {}),\n ...(wsAdapter !== undefined ? { wsAdapter } : {}),\n ...(triggersDaemon !== undefined ? { triggersDaemon } : {}),\n ...(consolidatorDaemon !== undefined ? { consolidatorDaemon } : {}),\n });\n\n // Start the consolidator first so it is ready to handle fired\n // triggers, bridge its cron / idle triggers onto the scheduler\n // (MCON-4 — without this nothing pipes triggers into the\n // consolidator and background consolidation never runs), then start\n // the scheduler last so it only fires fully-wired triggers.\n if (consolidatorDaemon !== undefined) {\n await consolidatorDaemon.start();\n if (\n triggersDaemon !== undefined &&\n consolidatorDaemon.consolidator.registerWithScheduler !== undefined\n ) {\n await consolidatorDaemon.consolidator.registerWithScheduler(triggersDaemon.scheduler);\n }\n }\n if (triggersDaemon !== undefined) {\n await triggersDaemon.start();\n }\n\n // Sample a couple of gauges immediately so the very first\n // `/v1/metrics` scrape after start carries non-zero data.\n try {\n const wal = readWalSize(store.connection);\n metricRegistry.set(SERVER_METRIC_NAMES.storageWalSize, wal);\n } catch {\n // Best-effort.\n }\n metricRegistry.set(\n SERVER_METRIC_NAMES.serverUptime,\n Math.max(0, Math.floor((now() - startedAt) / 1000)),\n );\n metricRegistry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());\n metricRegistry.set(SERVER_METRIC_NAMES.replayBufferEvents, 0);\n\n // IP-16: terminal run records (each holding an AbortController) would\n // otherwise accumulate forever; sweep them on a periodic timer.\n stopRunPruning = scheduleRunPruning(runs, now);\n\n if (options.skipListen !== true) {\n serverInstance = serve({\n fetch: app.fetch.bind(app),\n hostname: config.server.host,\n port: config.server.port,\n });\n if (wsAdapter !== undefined && serverInstance !== undefined) {\n wsAdapter.injectWebSocket(serverInstance);\n }\n await new Promise<void>((resolve) => {\n const server = serverInstance as unknown as {\n once(event: 'listening', cb: () => void): void;\n };\n if ('once' in server && typeof server.once === 'function') {\n server.once('listening', () => resolve());\n } else {\n // serve(...) returned a Node http.Server already bound.\n setImmediate(() => resolve());\n }\n });\n const address = (\n serverInstance as unknown as { address(): AddressInfo | string | null }\n ).address();\n if (address !== null && typeof address === 'object') {\n listening = { host: address.address, port: address.port };\n } else {\n listening = { host: config.server.host, port: config.server.port };\n }\n } else {\n listening = { host: config.server.host, port: config.server.port };\n }\n\n if (options.hooks?.onReady !== undefined) {\n await options.hooks.onReady({ config, listeningOn: listening });\n }\n return listening;\n } catch (err) {\n started = false;\n await emitError(options.hooks, { error: err, phase: 'beforeStart' });\n throw err;\n }\n },\n async stop({ force }: { readonly force?: boolean } = {}): Promise<void> {\n if (!started) throw new LifecycleNotStartedError();\n if (stopped) return;\n stopped = true;\n // IP-16: halt the prune sweep before draining.\n stopRunPruning?.();\n stopRunPruning = undefined;\n const drainTimeoutMs = force === true ? 0 : config.server.shutdown.drainTimeoutMs;\n try {\n if (options.hooks?.beforeShutdown !== undefined) {\n await options.hooks.beforeShutdown({\n config,\n inflight: runs.inflightCount(),\n drainTimeoutMs,\n });\n }\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n\n if (triggersDaemon !== undefined) {\n try {\n await triggersDaemon.stop();\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n }\n if (consolidatorDaemon !== undefined) {\n try {\n await consolidatorDaemon.stop();\n } catch (err) {\n await emitError(options.hooks, { error: err, phase: 'beforeShutdown' });\n }\n }\n\n // Pending reservations (e.g. awaited WS subscriptions for the\n // streaming endpoints) hold no work in progress; abort them\n // immediately so the drain only waits for live runs.\n runs.abortPending('server-shutdown');\n const drained = await drainInFlight(runs, drainTimeoutMs, now);\n if (!drained && drainTimeoutMs > 0) {\n // Force-abort everything that didn't drain in time.\n runs.abortAll(new ShutdownTimeoutError(drainTimeoutMs, runs.runningCount()));\n }\n\n if (dispatcher !== undefined) {\n try {\n dispatcher.shutdown();\n } catch {\n // Best-effort during stop().\n }\n }\n\n if (serverInstance !== undefined) {\n await new Promise<void>((resolve) => {\n (serverInstance as unknown as { close(cb: () => void): void }).close(() => resolve());\n });\n serverInstance = undefined;\n }\n if (auditDb !== undefined) {\n await auditDb.close();\n auditDb = undefined;\n }\n await store.close();\n listening = undefined;\n verifier = undefined;\n started = false;\n preBind = undefined;\n dispatcher = undefined;\n tickets = undefined;\n wsAdapter = undefined;\n },\n };\n return handle;\n}\n\ninterface MountRoutesContext {\n readonly version: string;\n readonly startedAt: number;\n readonly now: () => number;\n readonly agents: AgentRegistry;\n readonly workflows: WorkflowRegistry;\n readonly runs: RunStateTracker;\n readonly store: GraphorinSqliteStore;\n readonly metricRegistry: MetricRegistry;\n readonly sessions?: SessionApi;\n readonly memory?: MemoryApi;\n readonly skills?: SkillsApi;\n readonly mcp?: McpApi;\n readonly audit?: AuditApi;\n readonly replay?: ReplayApi;\n readonly healthProbes?: () => HealthCheckOptions | Promise<HealthCheckOptions>;\n readonly verifier?: TokenVerifier;\n readonly auditDb?: AuditDb;\n readonly pepper?: import('@graphorin/security').SecretValue;\n readonly wsDispatcher?: WsDispatcher;\n readonly wsTickets?: WsTicketStore;\n readonly wsAdapter?: ReturnType<typeof createNodeWebSocket>;\n readonly triggersDaemon?: TriggersDaemon;\n readonly consolidatorDaemon?: ConsolidatorDaemon;\n}\n\n/** IP-23: is `host` a loopback interface (so an open /metrics is not exposed)? */\nfunction isLoopbackHost(host: string): boolean {\n const h = host.trim().toLowerCase();\n return h === '127.0.0.1' || h === '::1' || h === '[::1]' || h === 'localhost';\n}\n\nfunction mountRoutes(\n app: Hono<{ Variables: ServerVariables }>,\n config: ServerConfigSpec,\n ctx: MountRoutesContext,\n): void {\n const base = config.server.basePath;\n const probes =\n ctx.healthProbes ??\n (() =>\n buildDefaultHealthProbes(\n ctx.store,\n ctx.triggersDaemon,\n ctx.consolidatorDaemon,\n ctx.wsDispatcher,\n config,\n ));\n const health = createExtendedHealthRoutes({\n version: ctx.version,\n startedAt: ctx.startedAt,\n now: ctx.now,\n probes,\n });\n app.route(`${base}/health`, health);\n\n // IP-13: `auth.kind='none'` disables authentication on every route. It is the\n // documented trusted-loopback / single-operator mode, but binding a\n // non-loopback host with auth off exposes full admin access (including the WS\n // stream) to anyone who can reach it — warn loudly rather than silently.\n if (config.auth.kind === 'none' && !isLoopbackHost(config.server.host)) {\n console.warn(\n `[graphorin/server] WARN: auth.kind='none' disables authentication on every endpoint, ` +\n `but the server binds the non-loopback host '${config.server.host}'. Anyone who can reach ` +\n `it has full admin access — use auth.kind='token' for non-loopback deployments or bind a ` +\n `loopback host.`,\n );\n }\n\n if (config.metrics.enabled) {\n // IP-23: an unauthenticated /metrics endpoint leaks operational intel\n // (trigger ids in labels, consolidator budgets). It is fine on a loopback\n // host, but binding a non-loopback host with auth off silently exposes it.\n if (!config.metrics.requireAuth && !isLoopbackHost(config.server.host)) {\n console.warn(\n `[graphorin/server] WARN: /metrics is unauthenticated (metrics.requireAuth=false) and ` +\n `the server binds the non-loopback host '${config.server.host}'. The exposition leaks ` +\n `operational detail to anyone who can reach it — set metrics.requireAuth=true or bind a ` +\n `loopback host.`,\n );\n }\n const metricsRoute = createMetricsRoutes({\n registry: ctx.metricRegistry,\n requireAuth: config.metrics.requireAuth,\n refresh: () =>\n refreshLiveMetrics({\n registry: ctx.metricRegistry,\n store: ctx.store,\n runs: ctx.runs,\n startedAt: ctx.startedAt,\n now: ctx.now,\n ...(ctx.triggersDaemon !== undefined ? { triggersDaemon: ctx.triggersDaemon } : {}),\n ...(ctx.consolidatorDaemon !== undefined\n ? { consolidatorDaemon: ctx.consolidatorDaemon }\n : {}),\n ...(ctx.wsDispatcher !== undefined ? { wsDispatcher: ctx.wsDispatcher } : {}),\n }),\n });\n app.route(`${base}${config.metrics.path}`, metricsRoute);\n }\n\n // Authenticated subtree begins here. The health endpoint above is\n // intentionally outside the auth boundary so liveness probes work\n // before the token verifier is wired. The WebSocket upgrade path is\n // also exempt: the upgrade handler in `ws/upgrade.ts` performs its\n // own bearer + ticket validation inline (the HTTP auth middleware\n // would otherwise short-circuit the upgrade with a 401 response\n // before the handler can negotiate the subprotocol).\n const wsUpgradePath = config.server.ws.enabled ? `${base}${config.server.ws.path}` : undefined;\n const metricsPath =\n config.metrics.enabled && !config.metrics.requireAuth\n ? `${base}${config.metrics.path}`\n : undefined;\n function shouldSkipAuth(path: string): boolean {\n if (path === `${base}/health` || path === `${base}/health/`) return true;\n if (wsUpgradePath !== undefined) {\n if (path === wsUpgradePath || path === `${wsUpgradePath}/`) return true;\n }\n if (metricsPath !== undefined) {\n if (path === metricsPath || path === `${metricsPath}/`) return true;\n }\n return false;\n }\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // verifier, but the authenticated subtree must still be reachable — install\n // an anonymous middleware that stamps a fully-authorized principal so the\n // domain routes serve instead of every one of them returning 401.\n const anonymousAuth = ctx.verifier === undefined && config.auth.kind === 'none';\n if (ctx.verifier !== undefined || anonymousAuth) {\n const authMw =\n ctx.verifier !== undefined\n ? createAuthMiddleware({ verifier: ctx.verifier })\n : createAnonymousAuthMiddleware();\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return authMw(c, next);\n });\n if (ctx.auditDb !== undefined) {\n const auditMw = createAuditMiddleware({ auditDb: ctx.auditDb, now: ctx.now });\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return auditMw(c, next);\n });\n }\n }\n\n // Idempotency middleware — applied to side-effecting endpoints. We\n // mount it once on the authenticated subtree so handlers don't have\n // to duplicate the configuration per-route.\n if (config.server.idempotency.enabled) {\n const idempotencyMw = createIdempotencyMiddleware({\n store: ctx.store.idempotency,\n config: config.server.idempotency,\n now: ctx.now,\n // IP-6: token minting returns a raw secret — never cache it.\n excludeResponseCachePaths: [`${base}/tokens`],\n // IP-15: publish the live cache hit ratio gauge.\n metricRegistry: ctx.metricRegistry,\n });\n app.use(`${base}/*`, async (c, next) => {\n if (shouldSkipAuth(c.req.path)) {\n await next();\n return;\n }\n return idempotencyMw(c, next);\n });\n }\n\n // Mounted AFTER the auth middleware so the scope check has a\n // verified token to inspect; the unauthenticated `/v1/health` GET\n // continues to serve the rollup.\n app.route(`${base}/health/secrets`, createSecretsHealthRoutes());\n\n app.route(\n `${base}/agents`,\n createAgentRoutes({\n agents: ctx.agents,\n runs: ctx.runs,\n // IP-2: the streaming dispatcher reaches the route layer.\n ...(ctx.wsDispatcher !== undefined ? { dispatcher: ctx.wsDispatcher } : {}),\n }),\n );\n app.route(`${base}/runs`, createRunRoutes({ agents: ctx.agents, runs: ctx.runs }));\n app.route(\n `${base}/workflows`,\n createWorkflowRoutes({\n workflows: ctx.workflows,\n runs: ctx.runs,\n ...(ctx.wsDispatcher !== undefined ? { dispatcher: ctx.wsDispatcher } : {}),\n }),\n );\n if (ctx.sessions !== undefined) {\n app.route(`${base}/sessions`, createSessionRoutes({ sessions: ctx.sessions }));\n }\n if (ctx.memory !== undefined) {\n app.route(`${base}/memory`, createMemoryRoutes({ memory: ctx.memory }));\n }\n if (ctx.skills !== undefined) {\n app.route(`${base}/skills`, createSkillsRoutes({ skills: ctx.skills }));\n }\n if (ctx.mcp !== undefined) {\n app.route(`${base}/mcp`, createMcpRoutes({ mcp: ctx.mcp }));\n }\n if (ctx.audit !== undefined) {\n app.route(`${base}/audit`, createAuditRoutes({ audit: ctx.audit }));\n }\n if (ctx.triggersDaemon !== undefined) {\n app.route(`${base}/triggers`, createTriggersRoutes({ daemon: ctx.triggersDaemon }));\n }\n if (ctx.replay !== undefined) {\n app.route(\n `${base}`,\n createReplayRoutes({\n replay: ctx.replay,\n ...(ctx.auditDb !== undefined ? { auditDb: ctx.auditDb } : {}),\n now: ctx.now,\n }),\n );\n }\n if (config.auth.kind === 'token' && ctx.pepper !== undefined) {\n app.route(\n `${base}/tokens`,\n createTokensRoutes({\n tokenStore: ctx.store.authTokens,\n pepper: ctx.pepper,\n defaultEnv: 'live',\n allowedEnvs: config.auth.tokenEnvironments,\n }),\n );\n }\n if (ctx.wsTickets !== undefined) {\n app.route(`${base}`, createAuthRoutes({ tickets: ctx.wsTickets }));\n }\n // IP-13: mount the WS upgrade when a verifier is wired OR auth is disabled\n // (`auth.kind='none'`). The old condition required a verifier, so\n // `ws.enabled: true` under no-auth was silently ignored — the route never\n // mounted and clients got a bare 404 with no explanation.\n if (\n ctx.wsDispatcher !== undefined &&\n ctx.wsTickets !== undefined &&\n ctx.wsAdapter !== undefined &&\n (ctx.verifier !== undefined || anonymousAuth) &&\n config.server.ws.enabled\n ) {\n const dispatcher = ctx.wsDispatcher;\n const tickets = ctx.wsTickets;\n const verifier = ctx.verifier;\n const runs = ctx.runs;\n app.get(\n `${base}${config.server.ws.path}`,\n ctx.wsAdapter.upgradeWebSocket((c) =>\n createWsUpgradeEvents(c, {\n dispatcher,\n tickets,\n ...(verifier !== undefined ? { verifier } : {}),\n anonymous: anonymousAuth,\n runs,\n now: ctx.now,\n }),\n ),\n );\n }\n if (ctx.wsDispatcher !== undefined && config.server.sse.enabled) {\n app.route(\n `${base}${config.server.sse.path}`,\n createSseRoutes({\n dispatcher: ctx.wsDispatcher,\n keepAliveMs: config.server.sse.keepAliveMs,\n // IP-9: bound the per-connection delivery queue.\n perConnectionQueueLimit: config.server.stream.perConnectionQueueLimit,\n now: ctx.now,\n }),\n );\n }\n}\n\nasync function drainInFlight(\n runs: RunStateTracker,\n drainTimeoutMs: number,\n now: () => number,\n): Promise<boolean> {\n if (runs.runningCount() === 0) return true;\n if (drainTimeoutMs <= 0) return runs.runningCount() === 0;\n const deadline = now() + drainTimeoutMs;\n while (now() < deadline) {\n if (runs.runningCount() === 0) return true;\n await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - now())));\n }\n return runs.runningCount() === 0;\n}\n\nasync function emitError(hooks: LifecycleHooks | undefined, ctx: OnErrorContext): Promise<void> {\n if (hooks?.onError === undefined) return;\n try {\n await hooks.onError(ctx);\n } catch {\n // onError must never throw further; swallow.\n }\n}\n\ninterface RefreshLiveMetricsOptions {\n readonly registry: MetricRegistry;\n readonly store: GraphorinSqliteStore;\n readonly runs: RunStateTracker;\n readonly startedAt: number;\n readonly now: () => number;\n readonly triggersDaemon?: TriggersDaemon;\n readonly consolidatorDaemon?: ConsolidatorDaemon;\n readonly wsDispatcher?: WsDispatcher;\n}\n\nasync function refreshLiveMetrics(options: RefreshLiveMetricsOptions): Promise<void> {\n const { registry, store, runs, startedAt, now } = options;\n\n try {\n const wal = readWalSize(store.connection);\n registry.set(SERVER_METRIC_NAMES.storageWalSize, wal);\n } catch {\n // Best-effort.\n }\n\n registry.set(\n SERVER_METRIC_NAMES.serverUptime,\n Math.max(0, Math.floor((now() - startedAt) / 1000)),\n );\n registry.set(SERVER_METRIC_NAMES.inflightRuns, runs.runningCount());\n\n if (options.wsDispatcher !== undefined) {\n const sizes = options.wsDispatcher.size();\n registry.set(SERVER_METRIC_NAMES.replayBufferEvents, sizes.subscriptions);\n }\n\n if (options.triggersDaemon !== undefined) {\n const metrics = options.triggersDaemon.metrics();\n for (const [triggerId, counts] of metrics.fires) {\n const sanitized = sanitizeMetricLabelValue(triggerId);\n const successCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {\n trigger_id: sanitized,\n status: 'success',\n });\n const errorCurrent = readCounter(registry, SERVER_METRIC_NAMES.triggersFiresTotal, {\n trigger_id: sanitized,\n status: 'error',\n });\n const successDelta = counts.success - successCurrent;\n const errorDelta = counts.error - errorCurrent;\n if (successDelta > 0) {\n registry.inc(\n SERVER_METRIC_NAMES.triggersFiresTotal,\n { trigger_id: sanitized, status: 'success' },\n successDelta,\n );\n }\n if (errorDelta > 0) {\n registry.inc(\n SERVER_METRIC_NAMES.triggersFiresTotal,\n { trigger_id: sanitized, status: 'error' },\n errorDelta,\n );\n }\n }\n }\n\n if (options.consolidatorDaemon !== undefined) {\n try {\n const status = await options.consolidatorDaemon.status();\n registry.set(SERVER_METRIC_NAMES.consolidatorQueueDepth, status.queueDepth, {\n phase: 'aggregate',\n });\n registry.set(SERVER_METRIC_NAMES.consolidatorDlqSize, status.dlqSize);\n registry.set(SERVER_METRIC_NAMES.consolidatorBudgetRemainingUsd, status.budget.costRemaining);\n registry.set(\n SERVER_METRIC_NAMES.consolidatorBudgetRemainingTokens,\n status.budget.tokensRemaining,\n );\n } catch {\n // Best-effort.\n }\n }\n}\n\n/**\n * Convert an arbitrary user-supplied identifier (trigger id) into a\n * Prometheus-safe label value. Replaces every character outside the\n * `[A-Za-z0-9_:]` range with `_`. This guarantees the cardinality\n * never explodes on UTF-8 sequences while keeping the value\n * recognizable.\n */\nfunction sanitizeMetricLabelValue(value: string): string {\n return value.replace(/[^A-Za-z0-9_:.-]/g, '_').slice(0, 200);\n}\n\nfunction readCounter(\n registry: MetricRegistry,\n name: string,\n labels: Record<string, string>,\n): number {\n const snap = registry.snapshot().counters[name] ?? [];\n for (const entry of snap) {\n if (matchesLabels(entry.labels, labels)) return entry.value;\n }\n return 0;\n}\n\nfunction matchesLabels(\n a: Record<string, string | number | boolean>,\n b: Record<string, string>,\n): boolean {\n for (const k of Object.keys(b)) {\n if (String(a[k]) !== b[k]) return false;\n }\n for (const k of Object.keys(a)) {\n if (!(k in b)) return false;\n }\n return true;\n}\n\nfunction buildDefaultHealthProbes(\n store: GraphorinSqliteStore,\n triggersDaemon: TriggersDaemon | undefined,\n consolidatorDaemon: ConsolidatorDaemon | undefined,\n dispatcher: WsDispatcher | undefined,\n config: ServerConfigSpec,\n): HealthCheckOptions {\n const out: {\n -readonly [K in keyof HealthCheckOptions]?: HealthCheckOptions[K];\n } = {\n store,\n walWarnThresholdBytes: config.health.walWarnThresholdBytes,\n encryptionEnabled: config.storage.encryption.enabled,\n // IP-1: when this process built the encrypted store itself, the\n // keyed open at boot proved the cipher peer — report the fact.\n ...(config.storage.encryption.enabled ? { cipherPeerInstalled: true } : {}),\n };\n if (triggersDaemon !== undefined) out.triggers = triggersDaemon;\n if (consolidatorDaemon !== undefined) out.consolidator = consolidatorDaemon;\n if (dispatcher !== undefined) {\n const sizes = dispatcher.size();\n // The dispatcher only exposes (subscribers, subscriptions); the\n // per-subject replay-buffer occupancy is owned by each\n // subscription, not the dispatcher itself. Surface the active\n // subscription count as a usable proxy + the raw subscriber count\n // so operators can correlate.\n out.replayBuffer = {\n eventsBuffered: sizes.subscriptions,\n subscribers: sizes.subscribers,\n subscriptions: sizes.subscriptions,\n };\n }\n return Object.freeze(out as HealthCheckOptions);\n}\n\nasync function fallbackPepper(skipHardening: boolean) {\n // Test-only path: when `skipHardening` is set we mint an ephemeral\n // pepper so the verifier is constructible without a real keyring.\n if (!skipHardening) {\n throw new Error(\n '[graphorin/server] missing resolved pepper after pre-bind; auth.pepperRef is required when auth.kind = token.',\n );\n }\n return generatePepper();\n}\n\nvoid process;\nvoid ConfigInvalidError;\n"],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { DeliveryCommentaryDecision, DeliveryCommentarySink } from "./types.js";
2
+ import { AuditDb, AuditEntryInput } from "@graphorin/security/audit";
3
+
4
+ //#region src/commentary/audit-bridge.d.ts
5
+
6
+ /**
7
+ * Audit action recorded for a delivery-commentary sanitization decision.
8
+ *
9
+ * @stable
10
+ */
11
+ declare const COMMENTARY_AUDIT_ACTION = "delivery:commentary:sanitized";
12
+ /**
13
+ * Translate a sanitizer decision into an audit entry. The digests + matched
14
+ * pattern bucket land in `metadata`; raw payloads never do (the sanitizer only
15
+ * ever exposes SHA-256s of the before/after bodies).
16
+ */
17
+ declare function commentaryDecisionToAuditInput(decision: DeliveryCommentaryDecision): AuditEntryInput;
18
+ /**
19
+ * A commentary sink that also exposes a `drain()` so callers (and tests) can
20
+ * await any in-flight audit writes.
21
+ *
22
+ * @stable
23
+ */
24
+ interface CommentaryAuditSink extends DeliveryCommentarySink {
25
+ /** Resolve once every queued audit write has settled. */
26
+ readonly drain: () => Promise<void>;
27
+ }
28
+ /**
29
+ * Build a commentary sink that appends each sanitization decision to the audit
30
+ * log. Writes serialise through `appendAudit` so concurrent decisions never
31
+ * race on `seq`; a failed write is isolated from the wire — `onWriteError`
32
+ * (default: a console warning) runs instead of throwing.
33
+ *
34
+ * @stable
35
+ */
36
+ declare function bridgeCommentaryToAudit(db: AuditDb, onWriteError?: (decision: DeliveryCommentaryDecision, error: unknown) => void): CommentaryAuditSink;
37
+ /**
38
+ * A {@link DeliveryCommentarySink} whose real target is installed later. The WS
39
+ * dispatcher is created before the audit DB opens (IP-21); the server hands it
40
+ * this forwarding sink and calls {@link LateBoundCommentarySink.bind} once the
41
+ * audit-writing sink exists. Decisions emitted before binding are dropped — the
42
+ * dispatcher only sanitizes once it is live (after `start()`, by which point
43
+ * the audit DB, if configured, has opened and bound).
44
+ *
45
+ * @stable
46
+ */
47
+ interface LateBoundCommentarySink extends DeliveryCommentarySink {
48
+ /** Install the real sink. Replaces any previously-bound target. */
49
+ bind(target: DeliveryCommentarySink): void;
50
+ }
51
+ /**
52
+ * @stable
53
+ */
54
+ declare function createLateBoundCommentarySink(): LateBoundCommentarySink;
55
+ //#endregion
56
+ export { COMMENTARY_AUDIT_ACTION, CommentaryAuditSink, LateBoundCommentarySink, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createLateBoundCommentarySink };
57
+ //# sourceMappingURL=audit-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-bridge.d.ts","names":[],"sources":["../../src/commentary/audit-bridge.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;cAyBa,uBAAA;;;;;;iBAOG,8BAAA,WACJ,6BACT;;;;;;;UAwBc,mBAAA,SAA4B;;wBAErB;;;;;;;;;;iBAWR,uBAAA,KACV,mCAEQ,sDAGX;;;;;;;;;;;UAiCc,uBAAA,SAAgC;;eAElC;;;;;iBAMC,6BAAA,CAAA,GAAiC"}
@@ -0,0 +1,92 @@
1
+ import { appendAudit } from "@graphorin/security/audit";
2
+
3
+ //#region src/commentary/audit-bridge.ts
4
+ /**
5
+ * IP-21: bridge the delivery-commentary sanitizer's audit decisions into the
6
+ * tamper-evident audit log. `createWsDispatcher` ran the sanitizer without a
7
+ * `sink`, so every documented sanitization decision — what was wrapped or
8
+ * stripped on the wire, with before/after digests and the matched-pattern
9
+ * bucket — was silently dropped instead of landing in the audit chain.
10
+ *
11
+ * The WS dispatcher is constructed before the audit DB is unsealed (the DB
12
+ * opens during `start()`), so the server hands the dispatcher a
13
+ * {@link LateBoundCommentarySink} and installs the real audit-writing target
14
+ * via {@link bridgeCommentaryToAudit} once the DB exists. Mirrors the
15
+ * `bridgeAuthToAudit` / `bridgeSecretsToAudit` pattern in `@graphorin/security`.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /**
20
+ * Audit action recorded for a delivery-commentary sanitization decision.
21
+ *
22
+ * @stable
23
+ */
24
+ const COMMENTARY_AUDIT_ACTION = "delivery:commentary:sanitized";
25
+ /**
26
+ * Translate a sanitizer decision into an audit entry. The digests + matched
27
+ * pattern bucket land in `metadata`; raw payloads never do (the sanitizer only
28
+ * ever exposes SHA-256s of the before/after bodies).
29
+ */
30
+ function commentaryDecisionToAuditInput(decision) {
31
+ return {
32
+ actor: {
33
+ kind: "system",
34
+ id: "graphorin/server"
35
+ },
36
+ action: COMMENTARY_AUDIT_ACTION,
37
+ target: `${decision.transport}:${decision.eventType}`,
38
+ decision: "success",
39
+ metadata: {
40
+ policy: decision.policy,
41
+ applied: decision.applied,
42
+ boundary: decision.boundary,
43
+ reasons: decision.reasons,
44
+ ...decision.matchedPattern !== void 0 ? { matchedPattern: decision.matchedPattern } : {},
45
+ sha256OfBefore: decision.sha256OfBefore,
46
+ sha256OfAfter: decision.sha256OfAfter
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Build a commentary sink that appends each sanitization decision to the audit
52
+ * log. Writes serialise through `appendAudit` so concurrent decisions never
53
+ * race on `seq`; a failed write is isolated from the wire — `onWriteError`
54
+ * (default: a console warning) runs instead of throwing.
55
+ *
56
+ * @stable
57
+ */
58
+ function bridgeCommentaryToAudit(db, onWriteError = defaultOnWriteError) {
59
+ let tail = Promise.resolve();
60
+ return {
61
+ onDecision(decision) {
62
+ const input = commentaryDecisionToAuditInput(decision);
63
+ tail = tail.then(() => appendAudit(db, input)).catch((error) => onWriteError(decision, error));
64
+ },
65
+ drain: async () => {
66
+ await tail;
67
+ }
68
+ };
69
+ }
70
+ function defaultOnWriteError(_decision, error) {
71
+ console.warn(`[graphorin/server] WARN: failed to write a delivery-commentary audit entry: ${error instanceof Error ? error.message : String(error)}`);
72
+ }
73
+ /**
74
+ * @stable
75
+ */
76
+ function createLateBoundCommentarySink() {
77
+ let target;
78
+ return {
79
+ bind(next) {
80
+ target = next;
81
+ },
82
+ onDecision(decision) {
83
+ try {
84
+ target?.onDecision(decision);
85
+ } catch {}
86
+ }
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ export { COMMENTARY_AUDIT_ACTION, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createLateBoundCommentarySink };
92
+ //# sourceMappingURL=audit-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-bridge.js","names":["tail: Promise<unknown>","target: DeliveryCommentarySink | undefined"],"sources":["../../src/commentary/audit-bridge.ts"],"sourcesContent":["/**\n * IP-21: bridge the delivery-commentary sanitizer's audit decisions into the\n * tamper-evident audit log. `createWsDispatcher` ran the sanitizer without a\n * `sink`, so every documented sanitization decision — what was wrapped or\n * stripped on the wire, with before/after digests and the matched-pattern\n * bucket — was silently dropped instead of landing in the audit chain.\n *\n * The WS dispatcher is constructed before the audit DB is unsealed (the DB\n * opens during `start()`), so the server hands the dispatcher a\n * {@link LateBoundCommentarySink} and installs the real audit-writing target\n * via {@link bridgeCommentaryToAudit} once the DB exists. Mirrors the\n * `bridgeAuthToAudit` / `bridgeSecretsToAudit` pattern in `@graphorin/security`.\n *\n * @packageDocumentation\n */\n\nimport { type AuditDb, type AuditEntryInput, appendAudit } from '@graphorin/security/audit';\n\nimport type { DeliveryCommentaryDecision, DeliveryCommentarySink } from './types.js';\n\n/**\n * Audit action recorded for a delivery-commentary sanitization decision.\n *\n * @stable\n */\nexport const COMMENTARY_AUDIT_ACTION = 'delivery:commentary:sanitized';\n\n/**\n * Translate a sanitizer decision into an audit entry. The digests + matched\n * pattern bucket land in `metadata`; raw payloads never do (the sanitizer only\n * ever exposes SHA-256s of the before/after bodies).\n */\nexport function commentaryDecisionToAuditInput(\n decision: DeliveryCommentaryDecision,\n): AuditEntryInput {\n return {\n actor: { kind: 'system', id: 'graphorin/server' },\n action: COMMENTARY_AUDIT_ACTION,\n target: `${decision.transport}:${decision.eventType}`,\n decision: 'success',\n metadata: {\n policy: decision.policy,\n applied: decision.applied,\n boundary: decision.boundary,\n reasons: decision.reasons,\n ...(decision.matchedPattern !== undefined ? { matchedPattern: decision.matchedPattern } : {}),\n sha256OfBefore: decision.sha256OfBefore,\n sha256OfAfter: decision.sha256OfAfter,\n },\n };\n}\n\n/**\n * A commentary sink that also exposes a `drain()` so callers (and tests) can\n * await any in-flight audit writes.\n *\n * @stable\n */\nexport interface CommentaryAuditSink extends DeliveryCommentarySink {\n /** Resolve once every queued audit write has settled. */\n readonly drain: () => Promise<void>;\n}\n\n/**\n * Build a commentary sink that appends each sanitization decision to the audit\n * log. Writes serialise through `appendAudit` so concurrent decisions never\n * race on `seq`; a failed write is isolated from the wire — `onWriteError`\n * (default: a console warning) runs instead of throwing.\n *\n * @stable\n */\nexport function bridgeCommentaryToAudit(\n db: AuditDb,\n onWriteError: (\n decision: DeliveryCommentaryDecision,\n error: unknown,\n ) => void = defaultOnWriteError,\n): CommentaryAuditSink {\n let tail: Promise<unknown> = Promise.resolve();\n return {\n onDecision(decision) {\n const input = commentaryDecisionToAuditInput(decision);\n tail = tail\n .then(() => appendAudit(db, input))\n .catch((error) => onWriteError(decision, error));\n },\n drain: async () => {\n await tail;\n },\n };\n}\n\nfunction defaultOnWriteError(_decision: DeliveryCommentaryDecision, error: unknown): void {\n console.warn(\n `[graphorin/server] WARN: failed to write a delivery-commentary audit entry: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n}\n\n/**\n * A {@link DeliveryCommentarySink} whose real target is installed later. The WS\n * dispatcher is created before the audit DB opens (IP-21); the server hands it\n * this forwarding sink and calls {@link LateBoundCommentarySink.bind} once the\n * audit-writing sink exists. Decisions emitted before binding are dropped — the\n * dispatcher only sanitizes once it is live (after `start()`, by which point\n * the audit DB, if configured, has opened and bound).\n *\n * @stable\n */\nexport interface LateBoundCommentarySink extends DeliveryCommentarySink {\n /** Install the real sink. Replaces any previously-bound target. */\n bind(target: DeliveryCommentarySink): void;\n}\n\n/**\n * @stable\n */\nexport function createLateBoundCommentarySink(): LateBoundCommentarySink {\n let target: DeliveryCommentarySink | undefined;\n return {\n bind(next) {\n target = next;\n },\n onDecision(decision) {\n // Contract: a sink never throws into the wire. The sanitizer also guards,\n // but be defensive in case a bound target misbehaves.\n try {\n target?.onDecision(decision);\n } catch {\n // swallow\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,0BAA0B;;;;;;AAOvC,SAAgB,+BACd,UACiB;AACjB,QAAO;EACL,OAAO;GAAE,MAAM;GAAU,IAAI;GAAoB;EACjD,QAAQ;EACR,QAAQ,GAAG,SAAS,UAAU,GAAG,SAAS;EAC1C,UAAU;EACV,UAAU;GACR,QAAQ,SAAS;GACjB,SAAS,SAAS;GAClB,UAAU,SAAS;GACnB,SAAS,SAAS;GAClB,GAAI,SAAS,mBAAmB,SAAY,EAAE,gBAAgB,SAAS,gBAAgB,GAAG,EAAE;GAC5F,gBAAgB,SAAS;GACzB,eAAe,SAAS;GACzB;EACF;;;;;;;;;;AAsBH,SAAgB,wBACd,IACA,eAGY,qBACS;CACrB,IAAIA,OAAyB,QAAQ,SAAS;AAC9C,QAAO;EACL,WAAW,UAAU;GACnB,MAAM,QAAQ,+BAA+B,SAAS;AACtD,UAAO,KACJ,WAAW,YAAY,IAAI,MAAM,CAAC,CAClC,OAAO,UAAU,aAAa,UAAU,MAAM,CAAC;;EAEpD,OAAO,YAAY;AACjB,SAAM;;EAET;;AAGH,SAAS,oBAAoB,WAAuC,OAAsB;AACxF,SAAQ,KACN,+EACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GAEzD;;;;;AAqBH,SAAgB,gCAAyD;CACvE,IAAIC;AACJ,QAAO;EACL,KAAK,MAAM;AACT,YAAS;;EAEX,WAAW,UAAU;AAGnB,OAAI;AACF,YAAQ,WAAW,SAAS;WACtB;;EAIX"}
@@ -0,0 +1,24 @@
1
+ import { DeliveryCommentaryPattern } from "./types.js";
2
+
3
+ //#region src/commentary/built-in-patterns.d.ts
4
+
5
+ /**
6
+ * The framework-shipped catalogue. Snapshot bytes-equal across the
7
+ * `ws` / `sse` / `rest` transports; idempotent on a single payload
8
+ * (the wrap envelope itself is not matched by any pattern, so a
9
+ * second pass over a previously-sanitized payload is a no-op).
10
+ *
11
+ * @stable
12
+ */
13
+ declare const DEFAULT_DELIVERY_COMMENTARY_PATTERNS: ReadonlyArray<DeliveryCommentaryPattern>;
14
+ /**
15
+ * Default whitelist of `event.type` strings the dispatcher walks
16
+ * through the sanitizer. Extension is opt-in via
17
+ * {@link import('./types.js').DeliveryCommentaryConfig}.applyToEvents.
18
+ *
19
+ * @stable
20
+ */
21
+ declare const DEFAULT_APPLY_TO_EVENTS: ReadonlyArray<string>;
22
+ //#endregion
23
+ export { DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS };
24
+ //# sourceMappingURL=built-in-patterns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-patterns.d.ts","names":[],"sources":["../../src/commentary/built-in-patterns.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;cA2Ba,sCAAsC,cAAc;;;;;;;;cA+CpD,yBAAyB"}
@@ -0,0 +1,62 @@
1
+ //#region src/commentary/built-in-patterns.ts
2
+ /**
3
+ * The framework-shipped catalogue. Snapshot bytes-equal across the
4
+ * `ws` / `sse` / `rest` transports; idempotent on a single payload
5
+ * (the wrap envelope itself is not matched by any pattern, so a
6
+ * second pass over a previously-sanitized payload is a no-op).
7
+ *
8
+ * @stable
9
+ */
10
+ const DEFAULT_DELIVERY_COMMENTARY_PATTERNS = Object.freeze([
11
+ Object.freeze({
12
+ reason: "tool.call.start-payload-signature",
13
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.start"[\s\S]*?"toolName"\s*:\s*"[^"]+"[\s\S]*?\}/g,
14
+ description: "JSON-encoded `tool.call.start` event with a `toolName` field — leaks the internal tool dispatch."
15
+ }),
16
+ Object.freeze({
17
+ reason: "tool.call.delta-payload-signature",
18
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.delta"[\s\S]*?"argsDelta"[\s\S]*?\}/g,
19
+ description: "JSON-encoded `tool.call.delta` event with streaming `argsDelta`."
20
+ }),
21
+ Object.freeze({
22
+ reason: "tool.call.end-payload-signature",
23
+ regex: /\{\s*"type"\s*:\s*"tool\.call\.end"[\s\S]*?"finalArgs"[\s\S]*?\}/g,
24
+ description: "JSON-encoded `tool.call.end` event carrying the final args payload."
25
+ }),
26
+ Object.freeze({
27
+ reason: "tool.execute.end-payload-signature",
28
+ regex: /\{\s*"type"\s*:\s*"tool\.execute\.end"[\s\S]*?"result"\s*:\s*[\s\S]*?\}/g,
29
+ description: "JSON-encoded `tool.execute.end` event with the raw `result` payload."
30
+ }),
31
+ Object.freeze({
32
+ reason: "agent.fanout-event-signature",
33
+ regex: /\{\s*"type"\s*:\s*"agent\.fanout\.(?:spawned|merged)"[\s\S]*?\}/g,
34
+ description: "JSON-encoded `agent.fanout.spawned` / `agent.fanout.merged` event."
35
+ }),
36
+ Object.freeze({
37
+ reason: "context.compacted-event-signature",
38
+ regex: /\{\s*"type"\s*:\s*"context\.compacted"[\s\S]*?"originalTokens"[\s\S]*?\}/g,
39
+ description: "JSON-encoded `context.compacted` event with token deltas."
40
+ }),
41
+ Object.freeze({
42
+ reason: "agent.model.fellback-event-signature",
43
+ regex: /\{\s*"type"\s*:\s*"agent\.model\.fellback"[\s\S]*?"fromModel"[\s\S]*?\}/g,
44
+ description: "JSON-encoded `agent.model.fellback` event with fallback chain detail."
45
+ })
46
+ ]);
47
+ /**
48
+ * Default whitelist of `event.type` strings the dispatcher walks
49
+ * through the sanitizer. Extension is opt-in via
50
+ * {@link import('./types.js').DeliveryCommentaryConfig}.applyToEvents.
51
+ *
52
+ * @stable
53
+ */
54
+ const DEFAULT_APPLY_TO_EVENTS = Object.freeze([
55
+ "tool.execute.end",
56
+ "tool.execute.error",
57
+ "text.delta"
58
+ ]);
59
+
60
+ //#endregion
61
+ export { DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS };
62
+ //# sourceMappingURL=built-in-patterns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-patterns.js","names":["DEFAULT_DELIVERY_COMMENTARY_PATTERNS: ReadonlyArray<DeliveryCommentaryPattern>","DEFAULT_APPLY_TO_EVENTS: ReadonlyArray<string>"],"sources":["../../src/commentary/built-in-patterns.ts"],"sourcesContent":["/**\n * Default pattern catalogue for the delivery-layer commentary-phase\n * trace sanitization applied by the WebSocket dispatcher (`@graphorin/server/ws`)\n * and the SSE event-emission boundary (`@graphorin/server/sse`).\n *\n * The catalogue is structurally identical to the session-output\n * sanitization catalogue exposed by `@graphorin/sessions/commentary`;\n * they live in two packages so the server's delivery layer does not\n * pull a hard dependency on the session module while still providing\n * defense-in-depth across the storage-write boundary (Phase 11) and\n * the wire-emission boundary (this module). Deployments that want a\n * single source of truth across both layers can pass the sessions\n * catalogue through {@link import('./types.js').DeliveryCommentaryConfig}.patterns.\n *\n * @packageDocumentation\n */\n\nimport type { DeliveryCommentaryPattern } from './types.js';\n\n/**\n * The framework-shipped catalogue. Snapshot bytes-equal across the\n * `ws` / `sse` / `rest` transports; idempotent on a single payload\n * (the wrap envelope itself is not matched by any pattern, so a\n * second pass over a previously-sanitized payload is a no-op).\n *\n * @stable\n */\nexport const DEFAULT_DELIVERY_COMMENTARY_PATTERNS: ReadonlyArray<DeliveryCommentaryPattern> =\n Object.freeze([\n Object.freeze({\n reason: 'tool.call.start-payload-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"tool\\.call\\.start\"[\\s\\S]*?\"toolName\"\\s*:\\s*\"[^\"]+\"[\\s\\S]*?\\}/g,\n description:\n 'JSON-encoded `tool.call.start` event with a `toolName` field — leaks the internal tool dispatch.',\n }),\n Object.freeze({\n reason: 'tool.call.delta-payload-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"tool\\.call\\.delta\"[\\s\\S]*?\"argsDelta\"[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `tool.call.delta` event with streaming `argsDelta`.',\n }),\n Object.freeze({\n reason: 'tool.call.end-payload-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"tool\\.call\\.end\"[\\s\\S]*?\"finalArgs\"[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `tool.call.end` event carrying the final args payload.',\n }),\n Object.freeze({\n reason: 'tool.execute.end-payload-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"tool\\.execute\\.end\"[\\s\\S]*?\"result\"\\s*:\\s*[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `tool.execute.end` event with the raw `result` payload.',\n }),\n Object.freeze({\n reason: 'agent.fanout-event-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"agent\\.fanout\\.(?:spawned|merged)\"[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `agent.fanout.spawned` / `agent.fanout.merged` event.',\n }),\n Object.freeze({\n reason: 'context.compacted-event-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"context\\.compacted\"[\\s\\S]*?\"originalTokens\"[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `context.compacted` event with token deltas.',\n }),\n Object.freeze({\n reason: 'agent.model.fellback-event-signature',\n regex: /\\{\\s*\"type\"\\s*:\\s*\"agent\\.model\\.fellback\"[\\s\\S]*?\"fromModel\"[\\s\\S]*?\\}/g,\n description: 'JSON-encoded `agent.model.fellback` event with fallback chain detail.',\n }),\n ]);\n\n/**\n * Default whitelist of `event.type` strings the dispatcher walks\n * through the sanitizer. Extension is opt-in via\n * {@link import('./types.js').DeliveryCommentaryConfig}.applyToEvents.\n *\n * @stable\n */\nexport const DEFAULT_APPLY_TO_EVENTS: ReadonlyArray<string> = Object.freeze([\n 'tool.execute.end',\n 'tool.execute.error',\n 'text.delta',\n]);\n"],"mappings":";;;;;;;;;AA2BA,MAAaA,uCACX,OAAO,OAAO;CACZ,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aACE;EACH,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACF,OAAO,OAAO;EACZ,QAAQ;EACR,OAAO;EACP,aAAa;EACd,CAAC;CACH,CAAC;;;;;;;;AASJ,MAAaC,0BAAiD,OAAO,OAAO;CAC1E;CACA;CACA;CACD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { DeliveryCommentaryConfig, DeliveryCommentaryDecision, DeliveryCommentaryPattern, DeliveryCommentaryPolicy, DeliveryCommentaryReason, DeliveryCommentarySink, DeliveryCommentaryTransport } from "./types.js";
2
+ import { COMMENTARY_AUDIT_ACTION, CommentaryAuditSink, LateBoundCommentarySink, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createLateBoundCommentarySink } from "./audit-bridge.js";
3
+ import { DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS } from "./built-in-patterns.js";
4
+ import { DeliveryCommentarySanitizer, createDeliveryCommentarySanitizer } from "./sanitizer.js";
5
+ export { COMMENTARY_AUDIT_ACTION, type CommentaryAuditSink, DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS, type DeliveryCommentaryConfig, type DeliveryCommentaryDecision, type DeliveryCommentaryPattern, type DeliveryCommentaryPolicy, type DeliveryCommentaryReason, type DeliveryCommentarySanitizer, type DeliveryCommentarySink, type DeliveryCommentaryTransport, type LateBoundCommentarySink, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createDeliveryCommentarySanitizer, createLateBoundCommentarySink };
@@ -0,0 +1,5 @@
1
+ import { COMMENTARY_AUDIT_ACTION, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createLateBoundCommentarySink } from "./audit-bridge.js";
2
+ import { DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS } from "./built-in-patterns.js";
3
+ import { createDeliveryCommentarySanitizer } from "./sanitizer.js";
4
+
5
+ export { COMMENTARY_AUDIT_ACTION, DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS, bridgeCommentaryToAudit, commentaryDecisionToAuditInput, createDeliveryCommentarySanitizer, createLateBoundCommentarySink };
@@ -0,0 +1,37 @@
1
+ import { DeliveryCommentaryConfig, DeliveryCommentaryPattern, DeliveryCommentaryPolicy, DeliveryCommentaryTransport } from "./types.js";
2
+ import { ServerEventFrame } from "@graphorin/protocol";
3
+
4
+ //#region src/commentary/sanitizer.d.ts
5
+
6
+ /**
7
+ * Public surface returned by {@link createDeliveryCommentarySanitizer}.
8
+ *
9
+ * @stable
10
+ */
11
+ interface DeliveryCommentarySanitizer {
12
+ readonly policy: DeliveryCommentaryPolicy;
13
+ readonly applyToEvents: ReadonlyArray<string>;
14
+ readonly patterns: ReadonlyArray<DeliveryCommentaryPattern>;
15
+ /**
16
+ * Sanitize the payload of a single `event` frame. Returns the
17
+ * (possibly-replaced) frame; emits an audit decision via the
18
+ * configured sink when the sanitizer mutated the payload.
19
+ *
20
+ * The frame is returned unchanged when:
21
+ * - the policy is `'pass-through'`,
22
+ * - the event type is not in `applyToEvents`,
23
+ * - or no pattern matched the JSON-stringified payload.
24
+ */
25
+ sanitize(frame: ServerEventFrame, transport: DeliveryCommentaryTransport): ServerEventFrame;
26
+ }
27
+ /**
28
+ * Build a stateless delivery-layer sanitizer. Tests can swap the
29
+ * `sink` for an in-memory recorder; production wires the
30
+ * `@graphorin/security/audit` `appendAudit` helper.
31
+ *
32
+ * @stable
33
+ */
34
+ declare function createDeliveryCommentarySanitizer(config?: DeliveryCommentaryConfig): DeliveryCommentarySanitizer;
35
+ //#endregion
36
+ export { DeliveryCommentarySanitizer, createDeliveryCommentarySanitizer };
37
+ //# sourceMappingURL=sanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizer.d.ts","names":[],"sources":["../../src/commentary/sanitizer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UA6CiB,2BAAA;mBACE;0BACO;qBACL,cAAc;;;;;;;;;;;kBAWjB,6BAA6B,8BAA8B;;;;;;;;;iBAU7D,iCAAA,UACN,2BACP"}