@graphorin/server 0.5.0 → 0.6.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.
- package/CHANGELOG.md +36 -0
- package/README.md +6 -6
- package/dist/app.d.ts +5 -5
- package/dist/app.js +3 -3
- package/dist/app.js.map +1 -1
- package/dist/commentary/audit-bridge.d.ts +2 -2
- package/dist/commentary/audit-bridge.js +3 -3
- package/dist/commentary/audit-bridge.js.map +1 -1
- package/dist/commentary/built-in-patterns.js +1 -1
- package/dist/commentary/built-in-patterns.js.map +1 -1
- package/dist/commentary/sanitizer.js +2 -2
- package/dist/commentary/sanitizer.js.map +1 -1
- package/dist/commentary/types.d.ts +5 -5
- package/dist/consolidator/daemon.d.ts +1 -1
- package/dist/consolidator/daemon.js.map +1 -1
- package/dist/health/checks.js.map +1 -1
- package/dist/health/routes.d.ts +2 -2
- package/dist/health/routes.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/ids.js +1 -1
- package/dist/internal/ids.js.map +1 -1
- package/dist/lifecycle/pre-bind.js.map +1 -1
- package/dist/metrics/catalog.js +1 -1
- package/dist/metrics/catalog.js.map +1 -1
- package/dist/metrics/registry.d.ts +2 -2
- package/dist/metrics/registry.js +1 -1
- package/dist/metrics/registry.js.map +1 -1
- package/dist/middleware/auth.d.ts +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/cors.js.map +1 -1
- package/dist/middleware/csrf.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +1 -1
- package/dist/middleware/idempotency.js +36 -23
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/middleware/request-state.js.map +1 -1
- package/dist/middleware/scope.js.map +1 -1
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry/index.js +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/replay/routes.js +1 -1
- package/dist/replay/routes.js.map +1 -1
- package/dist/routes/agents.js +1 -1
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/audit.d.ts +1 -1
- package/dist/routes/audit.js.map +1 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/health.js +1 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/workflows.js +42 -8
- package/dist/routes/workflows.js.map +1 -1
- package/dist/runtime/run-state.d.ts +3 -3
- package/dist/runtime/run-state.js +3 -3
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/sse/events.js +1 -1
- package/dist/sse/events.js.map +1 -1
- package/dist/triggers/daemon.js.map +1 -1
- package/dist/triggers/routes.js.map +1 -1
- package/dist/ws/dispatcher.d.ts.map +1 -1
- package/dist/ws/dispatcher.js.map +1 -1
- package/dist/ws/subjects.d.ts +1 -1
- package/dist/ws/subjects.d.ts.map +1 -1
- package/dist/ws/subjects.js +3 -3
- package/dist/ws/subjects.js.map +1 -1
- package/dist/ws/ticket.d.ts +1 -1
- package/dist/ws/ticket.js +1 -1
- package/dist/ws/ticket.js.map +1 -1
- package/dist/ws/upgrade.js +6 -5
- package/dist/ws/upgrade.js.map +1 -1
- package/package.json +17 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @graphorin/server
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - Server / client / periphery correctness (audit 2026-07-04 Wave B, cluster B6).
|
|
8
|
+
|
|
9
|
+
- periphery-01: `POST /v1/workflows/:id/resume` actually resumes - it background-iterates `workflow.resume(threadId, {resume})` and emits every event on the `workflow:<id>/runs/<runId>/events` subject (mirroring execute); the fictional unmounted SSE URL is gone and the run completes. `POST /v1/workflows/:id/fork` returns an honest 501 instead of a 202 that forked nothing.
|
|
10
|
+
- periphery-02: `standalone-server.md` now states plainly that `graphorin start` serves health/metrics/tokens/tickets/WS-SSE only and that the domain routes mount only when adapters are composed programmatically via `createServer({...})`.
|
|
11
|
+
- periphery-03: the client SSE fallback survives reconnects - `#reconnect` no longer attempts the RPC resubscribe on the read-only SSE transport (which threw and permanently killed the subscription); the reconnect carries the subscription's cursor as a `Last-Event-ID` header so the server replays only missed events; a closed bound subscription is recreated on the next `subscribe()`.
|
|
12
|
+
- periphery-08: the idempotency middleware tracks in-flight keyed executions - a concurrent duplicate gets `409 idempotency-in-flight` + `Retry-After` instead of double-executing (per draft-ietf-httpapi-idempotency-key-header).
|
|
13
|
+
- periphery-09: `subscribe({ target: 'run' })` without `sessionId` throws a clear client-side error instead of building a `run:` subject the server grammar can never accept.
|
|
14
|
+
- periphery-10: session-stream subjects gate on `sessions:read:<sessionId>` (read-only streams, sessionId resource slot) instead of `agents:invoke:<sessionId>`, aligning with the SSE route's requirement.
|
|
15
|
+
- periphery-11: interval triggers under `catchupPolicy: 'none'` advance to the next FUTURE boundary after downtime instead of firing immediately on restart; `recordActivity()` no longer arms idle timers on a stopped scheduler (P-14).
|
|
16
|
+
- periphery-05: the TransformersJS embedder throws for an unknown model with no `dim` hint (the PS-11 fix ported from the Ollama embedder) instead of assuming 768 - a wrong assumed width baked a wrong-width id + vec0 table and the id changed after the first embed; a width drift against a bound dim now throws too.
|
|
17
|
+
- P-05: the WS upgrade bearer verification passes the client IP so the per-IP failure lockout engages for upgrade attempts (previously a lockout-free brute-force surface).
|
|
18
|
+
- Docs: the fictional "disconnect policy" section replaced with the real reconnect-and-replay behaviour; the WS ticket endpoint path corrected to `/v1/session/ws-ticket`.
|
|
19
|
+
|
|
20
|
+
- [#138](https://github.com/o-stepper/graphorin/pull/138) [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a) Thanks [@o-stepper](https://github.com/o-stepper)! - Release-pipeline and tarball-surface fixes (audit 2026-07-04 Wave E, cluster E2). `@graphorin/memory`'s `./conflict` subpath was runtime-EMPTY on npm 0.5.0 (`export { };` - preserveModules emitted the module without an explicit tsdown entry); it now ships all 8 runtime exports. `@graphorin/server`'s internal `workspace:*` peer dependencies are ranged (`workspace:>=0.5.0 <1.0.0`) so changesets stops escalating every sibling bump into a bogus MAJOR for the whole fixed group (the 1.0.0 landmine on the release bot's branch). `@graphorin/eslint-plugin` gains the `./package.json` self-export. All 27 per-package CHANGELOGs gain the 0.5.0 section (they were frozen at 0.1.0 inside every published tarball), and the `mvp-readiness` release gate now rejects a stale-CHANGELOG or unresolvable-exports release.
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a)]:
|
|
23
|
+
- @graphorin/store-sqlite@0.6.0
|
|
24
|
+
- @graphorin/core@0.6.0
|
|
25
|
+
- @graphorin/triggers@0.6.0
|
|
26
|
+
- @graphorin/security@0.6.0
|
|
27
|
+
- @graphorin/observability@0.6.0
|
|
28
|
+
- @graphorin/protocol@0.6.0
|
|
29
|
+
|
|
30
|
+
## 0.5.0
|
|
31
|
+
|
|
32
|
+
First version published to the npm registry (with Sigstore build
|
|
33
|
+
provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
|
|
34
|
+
milestones and were never published. All `@graphorin/*` packages release
|
|
35
|
+
lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
|
|
36
|
+
in the repository-level
|
|
37
|
+
[CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
|
|
38
|
+
|
|
3
39
|
## 0.1.0
|
|
4
40
|
|
|
5
41
|
Initial release. See the workspace root `CHANGELOG.md` for the full
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
- **Version:** v0.
|
|
8
|
+
- **Version:** v0.6.0
|
|
9
9
|
- **License:** [MIT](./LICENSE) (© 2026 Oleksiy Stepurenko)
|
|
10
10
|
- **Repository:** <https://github.com/o-stepper/graphorin/tree/main/packages/server>
|
|
11
11
|
- **Issues:** <https://github.com/o-stepper/graphorin/issues>
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
| **Prometheus metrics** | `/v1/metrics` text exposition v0.0.4 with `graphorin_*` prefix discipline. The canonical inventory covers agent runs, tool calls, provider tokens / cost, WAL size, idempotency hit ratio, trigger fires, redaction drops, consolidator queue depth / DLQ / budget, OAuth token freshness, replay buffer size, in-flight runs, server uptime, and a static `graphorin_build_info` gauge. |
|
|
34
34
|
| **Replay endpoints** | `POST /v1/runs/:runId/replay` and `POST /v1/sessions/:id/replay` enforce `traces:read:sanitized` (default) or `traces:read:raw` (admin); every invocation appends an audit chain entry. |
|
|
35
35
|
| **Graceful shutdown** | Drains in-flight runs within the configured timeout (default 30 s), preserves run state via `RunStateTracker`, propagates `AbortSignal` cancellation to handlers, tears down WebSocket subscriptions cleanly. |
|
|
36
|
-
| **No phone home** | Zero implicit network calls
|
|
36
|
+
| **No phone home** | Zero implicit network calls - verified by the repository-wide `pnpm run check-no-network` CI check. |
|
|
37
37
|
|
|
38
38
|
## Out of scope for Phase 14
|
|
39
39
|
|
|
40
|
-
- Multi-tenant runtime
|
|
41
|
-
- OpenTelemetry metrics export (in addition to Prometheus)
|
|
40
|
+
- Multi-tenant runtime - **v0.3+**.
|
|
41
|
+
- OpenTelemetry metrics export (in addition to Prometheus) - post-MVP.
|
|
42
42
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
@@ -109,8 +109,8 @@ The most-frequently-touched options:
|
|
|
109
109
|
|
|
110
110
|
## Status
|
|
111
111
|
|
|
112
|
-
`@graphorin/server` is part of the Graphorin framework's `v0.
|
|
112
|
+
`@graphorin/server` is part of the Graphorin framework's `v0.6.0` pre-release. Once published, the package follows the lockstep release cadence shared by every `@graphorin/*` package on the `0.x` line.
|
|
113
113
|
|
|
114
114
|
---
|
|
115
115
|
|
|
116
|
-
**Project Graphorin** · v0.
|
|
116
|
+
**Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
package/dist/app.d.ts
CHANGED
|
@@ -59,13 +59,13 @@ interface GraphorinServer {
|
|
|
59
59
|
*/
|
|
60
60
|
readonly wsTickets: WsTicketStore | undefined;
|
|
61
61
|
/**
|
|
62
|
-
* Optional triggers daemon
|
|
62
|
+
* Optional triggers daemon - populated when the operator wired a
|
|
63
63
|
* scheduler (or an in-process trigger surface) at construction
|
|
64
64
|
* time. Phase 14c integration.
|
|
65
65
|
*/
|
|
66
66
|
readonly triggers: TriggersDaemon | undefined;
|
|
67
67
|
/**
|
|
68
|
-
* Optional consolidator daemon
|
|
68
|
+
* Optional consolidator daemon - populated when the operator
|
|
69
69
|
* supplied a `Consolidator` instance via `createServer({
|
|
70
70
|
* consolidator })`. Phase 14c integration.
|
|
71
71
|
*/
|
|
@@ -86,7 +86,7 @@ interface GraphorinServer {
|
|
|
86
86
|
/**
|
|
87
87
|
* Discriminated union accepted by `CreateServerOptions.triggers`. A
|
|
88
88
|
* caller may either supply a fully-built daemon (e.g. constructed
|
|
89
|
-
* around a custom `Scheduler`) or just the underlying scheduler
|
|
89
|
+
* around a custom `Scheduler`) or just the underlying scheduler - the
|
|
90
90
|
* server wraps it with {@link createTriggersDaemon} automatically.
|
|
91
91
|
*
|
|
92
92
|
* @stable
|
|
@@ -100,7 +100,7 @@ type TriggersDaemonInput = {
|
|
|
100
100
|
* @stable
|
|
101
101
|
*/
|
|
102
102
|
interface CreateServerOptions {
|
|
103
|
-
/** Loaded `graphorin.config.ts` payload
|
|
103
|
+
/** Loaded `graphorin.config.ts` payload - see `defineConfig({...})`. */
|
|
104
104
|
readonly config?: ServerConfigInput;
|
|
105
105
|
/**
|
|
106
106
|
* Optional pre-validated config. When supplied, `config` is ignored
|
|
@@ -128,7 +128,7 @@ interface CreateServerOptions {
|
|
|
128
128
|
*/
|
|
129
129
|
readonly consolidator?: ConsolidatorLike;
|
|
130
130
|
/**
|
|
131
|
-
* Optional triggers daemon
|
|
131
|
+
* Optional triggers daemon - pass an existing one (e.g. built
|
|
132
132
|
* from `createScheduler`) or a triggers configuration the server
|
|
133
133
|
* should wrap with the daemon adapter.
|
|
134
134
|
*/
|
package/dist/app.js
CHANGED
|
@@ -142,7 +142,7 @@ function rowToEntry(row) {
|
|
|
142
142
|
/**
|
|
143
143
|
* @stable
|
|
144
144
|
*/
|
|
145
|
-
const VERSION = "0.
|
|
145
|
+
const VERSION = "0.6.0";
|
|
146
146
|
/**
|
|
147
147
|
* Build a fully-wired Graphorin server. The returned handle is
|
|
148
148
|
* inert until `start()` is awaited.
|
|
@@ -450,9 +450,9 @@ function mountRoutes(app, config, ctx) {
|
|
|
450
450
|
probes
|
|
451
451
|
});
|
|
452
452
|
app.route(`${base}/health`, health);
|
|
453
|
-
if (config.auth.kind === "none" && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: auth.kind='none' disables authentication on every endpoint, but the server binds the non-loopback host '${config.server.host}'. Anyone who can reach it has full admin access
|
|
453
|
+
if (config.auth.kind === "none" && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: auth.kind='none' disables authentication on every endpoint, but the server binds the non-loopback host '${config.server.host}'. Anyone who can reach it has full admin access - use auth.kind='token' for non-loopback deployments or bind a loopback host.`);
|
|
454
454
|
if (config.metrics.enabled) {
|
|
455
|
-
if (!config.metrics.requireAuth && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: /metrics is unauthenticated (metrics.requireAuth=false) and the server binds the non-loopback host '${config.server.host}'. The exposition leaks operational detail to anyone who can reach it
|
|
455
|
+
if (!config.metrics.requireAuth && !isLoopbackHost(config.server.host)) console.warn(`[graphorin/server] WARN: /metrics is unauthenticated (metrics.requireAuth=false) and the server binds the non-loopback host '${config.server.host}'. The exposition leaks operational detail to anyone who can reach it - set metrics.requireAuth=true or bind a loopback host.`);
|
|
456
456
|
const metricsRoute = createMetricsRoutes({
|
|
457
457
|
registry: ctx.metricRegistry,
|
|
458
458
|
requireAuth: config.metrics.requireAuth,
|
package/dist/app.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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.6.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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"}
|
|
@@ -28,7 +28,7 @@ interface CommentaryAuditSink extends DeliveryCommentarySink {
|
|
|
28
28
|
/**
|
|
29
29
|
* Build a commentary sink that appends each sanitization decision to the audit
|
|
30
30
|
* log. Writes serialise through `appendAudit` so concurrent decisions never
|
|
31
|
-
* race on `seq`; a failed write is isolated from the wire
|
|
31
|
+
* race on `seq`; a failed write is isolated from the wire - `onWriteError`
|
|
32
32
|
* (default: a console warning) runs instead of throwing.
|
|
33
33
|
*
|
|
34
34
|
* @stable
|
|
@@ -38,7 +38,7 @@ declare function bridgeCommentaryToAudit(db: AuditDb, onWriteError?: (decision:
|
|
|
38
38
|
* A {@link DeliveryCommentarySink} whose real target is installed later. The WS
|
|
39
39
|
* dispatcher is created before the audit DB opens (IP-21); the server hands it
|
|
40
40
|
* this forwarding sink and calls {@link LateBoundCommentarySink.bind} once the
|
|
41
|
-
* audit-writing sink exists. Decisions emitted before binding are dropped
|
|
41
|
+
* audit-writing sink exists. Decisions emitted before binding are dropped - the
|
|
42
42
|
* dispatcher only sanitizes once it is live (after `start()`, by which point
|
|
43
43
|
* the audit DB, if configured, has opened and bound).
|
|
44
44
|
*
|
|
@@ -4,9 +4,9 @@ import { appendAudit } from "@graphorin/security/audit";
|
|
|
4
4
|
/**
|
|
5
5
|
* IP-21: bridge the delivery-commentary sanitizer's audit decisions into the
|
|
6
6
|
* tamper-evident audit log. `createWsDispatcher` ran the sanitizer without a
|
|
7
|
-
* `sink`, so every documented sanitization decision
|
|
7
|
+
* `sink`, so every documented sanitization decision - what was wrapped or
|
|
8
8
|
* stripped on the wire, with before/after digests and the matched-pattern
|
|
9
|
-
* bucket
|
|
9
|
+
* bucket - was silently dropped instead of landing in the audit chain.
|
|
10
10
|
*
|
|
11
11
|
* The WS dispatcher is constructed before the audit DB is unsealed (the DB
|
|
12
12
|
* opens during `start()`), so the server hands the dispatcher a
|
|
@@ -50,7 +50,7 @@ function commentaryDecisionToAuditInput(decision) {
|
|
|
50
50
|
/**
|
|
51
51
|
* Build a commentary sink that appends each sanitization decision to the audit
|
|
52
52
|
* log. Writes serialise through `appendAudit` so concurrent decisions never
|
|
53
|
-
* race on `seq`; a failed write is isolated from the wire
|
|
53
|
+
* race on `seq`; a failed write is isolated from the wire - `onWriteError`
|
|
54
54
|
* (default: a console warning) runs instead of throwing.
|
|
55
55
|
*
|
|
56
56
|
* @stable
|
|
@@ -1 +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
|
|
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"}
|
|
@@ -11,7 +11,7 @@ const DEFAULT_DELIVERY_COMMENTARY_PATTERNS = Object.freeze([
|
|
|
11
11
|
Object.freeze({
|
|
12
12
|
reason: "tool.call.start-payload-signature",
|
|
13
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
|
|
14
|
+
description: "JSON-encoded `tool.call.start` event with a `toolName` field - leaks the internal tool dispatch."
|
|
15
15
|
}),
|
|
16
16
|
Object.freeze({
|
|
17
17
|
reason: "tool.call.delta-payload-signature",
|
|
@@ -1 +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
|
|
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"}
|
|
@@ -8,14 +8,14 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
* pattern catalogue to the JSON-stringified payload; matched
|
|
9
9
|
* fragments are wrapped in the configured envelope (default) or
|
|
10
10
|
* stripped (per the `'strip'` policy) or passed through (per the
|
|
11
|
-
* `'pass-through'` policy
|
|
11
|
+
* `'pass-through'` policy - operator opt-out for trusted
|
|
12
12
|
* deployments).
|
|
13
13
|
*
|
|
14
14
|
* The sanitizer runs AFTER the `ServerMessage` Zod-validation pass
|
|
15
15
|
* in `@graphorin/protocol` and BEFORE the message hits the wire (so
|
|
16
16
|
* the protocol schema is unchanged; the sanitization is a pure
|
|
17
17
|
* transform applied to validated payloads). It is bytes-equal across
|
|
18
|
-
* the WS / SSE / REST transports
|
|
18
|
+
* the WS / SSE / REST transports - the same input + same policy
|
|
19
19
|
* produces the same output regardless of which transport invoked
|
|
20
20
|
* the sanitizer.
|
|
21
21
|
*
|