@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +6 -6
  3. package/dist/app.d.ts +5 -5
  4. package/dist/app.js +3 -3
  5. package/dist/app.js.map +1 -1
  6. package/dist/commentary/audit-bridge.d.ts +2 -2
  7. package/dist/commentary/audit-bridge.js +3 -3
  8. package/dist/commentary/audit-bridge.js.map +1 -1
  9. package/dist/commentary/built-in-patterns.js +1 -1
  10. package/dist/commentary/built-in-patterns.js.map +1 -1
  11. package/dist/commentary/sanitizer.js +2 -2
  12. package/dist/commentary/sanitizer.js.map +1 -1
  13. package/dist/commentary/types.d.ts +5 -5
  14. package/dist/consolidator/daemon.d.ts +1 -1
  15. package/dist/consolidator/daemon.js.map +1 -1
  16. package/dist/health/checks.js.map +1 -1
  17. package/dist/health/routes.d.ts +2 -2
  18. package/dist/health/routes.js.map +1 -1
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.js +7 -7
  21. package/dist/index.js.map +1 -1
  22. package/dist/internal/ids.js +1 -1
  23. package/dist/internal/ids.js.map +1 -1
  24. package/dist/lifecycle/pre-bind.js.map +1 -1
  25. package/dist/metrics/catalog.js +1 -1
  26. package/dist/metrics/catalog.js.map +1 -1
  27. package/dist/metrics/registry.d.ts +2 -2
  28. package/dist/metrics/registry.js +1 -1
  29. package/dist/metrics/registry.js.map +1 -1
  30. package/dist/middleware/auth.d.ts +1 -1
  31. package/dist/middleware/auth.js +1 -1
  32. package/dist/middleware/auth.js.map +1 -1
  33. package/dist/middleware/cors.js.map +1 -1
  34. package/dist/middleware/csrf.js.map +1 -1
  35. package/dist/middleware/idempotency.d.ts +1 -1
  36. package/dist/middleware/idempotency.js +36 -23
  37. package/dist/middleware/idempotency.js.map +1 -1
  38. package/dist/middleware/rate-limit.js.map +1 -1
  39. package/dist/middleware/request-state.js.map +1 -1
  40. package/dist/middleware/scope.js.map +1 -1
  41. package/dist/registry/index.d.ts +2 -2
  42. package/dist/registry/index.js +1 -1
  43. package/dist/registry/index.js.map +1 -1
  44. package/dist/replay/routes.js +1 -1
  45. package/dist/replay/routes.js.map +1 -1
  46. package/dist/routes/agents.js +1 -1
  47. package/dist/routes/agents.js.map +1 -1
  48. package/dist/routes/audit.d.ts +1 -1
  49. package/dist/routes/audit.js.map +1 -1
  50. package/dist/routes/auth.js.map +1 -1
  51. package/dist/routes/health.js +1 -1
  52. package/dist/routes/health.js.map +1 -1
  53. package/dist/routes/sessions.js.map +1 -1
  54. package/dist/routes/tokens.js.map +1 -1
  55. package/dist/routes/workflows.js +42 -8
  56. package/dist/routes/workflows.js.map +1 -1
  57. package/dist/runtime/run-state.d.ts +3 -3
  58. package/dist/runtime/run-state.js +3 -3
  59. package/dist/runtime/run-state.js.map +1 -1
  60. package/dist/sse/events.js +1 -1
  61. package/dist/sse/events.js.map +1 -1
  62. package/dist/triggers/daemon.js.map +1 -1
  63. package/dist/triggers/routes.js.map +1 -1
  64. package/dist/ws/dispatcher.d.ts.map +1 -1
  65. package/dist/ws/dispatcher.js.map +1 -1
  66. package/dist/ws/subjects.d.ts +1 -1
  67. package/dist/ws/subjects.d.ts.map +1 -1
  68. package/dist/ws/subjects.js +3 -3
  69. package/dist/ws/subjects.js.map +1 -1
  70. package/dist/ws/ticket.d.ts +1 -1
  71. package/dist/ws/ticket.js +1 -1
  72. package/dist/ws/ticket.js.map +1 -1
  73. package/dist/ws/upgrade.js +6 -5
  74. package/dist/ws/upgrade.js.map +1 -1
  75. package/package.json +17 -17
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.js","names":["lastFireAtMs: number | undefined","pump: Promise<void> | undefined","out: {\n -readonly [K in keyof TriggersDaemonStatus]?: TriggersDaemonStatus[K];\n }"],"sources":["../../src/triggers/daemon.ts"],"sourcesContent":["/**\n * Trigger scheduler integration with the server lifecycle.\n *\n * The daemon wraps a `Scheduler` from `@graphorin/triggers` and binds\n * its `start()` / `stop()` calls to the `beforeStart` / `beforeShutdown`\n * lifecycle hooks. On `start()`, every persisted trigger is loaded\n * from `trigger_state` (durable) and the per-trigger `catchupPolicy`\n * is honoured so cron / interval / idle / event triggers survive\n * process restarts as documented in the runtime spec.\n *\n * The daemon never invokes user callbacks itself those run inside\n * the `Scheduler`. The daemon only owns lifecycle + observability.\n *\n * @packageDocumentation\n */\n\nimport type { CatchupPolicy, Scheduler, SchedulerEvent } from '@graphorin/triggers';\n\n/**\n * Snapshot exposed via {@link TriggersDaemon.status} + the\n * `/v1/health` aggregator. Counts split by `disabled` so the health\n * endpoint can flag a deployment that disabled every cron trigger.\n *\n * @stable\n */\nexport interface TriggersDaemonStatus {\n readonly running: boolean;\n readonly active: number;\n readonly disabled: number;\n readonly deferred: number;\n /** Last fire timestamp across the entire pool (ISO-8601). */\n readonly lastFireAt?: string;\n}\n\n/**\n * Aggregate counters surfaced through the Prometheus\n * `/v1/metrics` exposition. Updated incrementally as the scheduler\n * publishes lifecycle events.\n *\n * @stable\n */\nexport interface TriggersDaemonMetrics {\n readonly fires: ReadonlyMap<string, { readonly success: number; readonly error: number }>;\n readonly catchupApplied: number;\n readonly libModeWarnings: number;\n}\n\n/**\n * @stable\n */\nexport interface CreateTriggersDaemonOptions {\n readonly scheduler: Scheduler;\n /**\n * Optional logger. Defaults to the standard error stream so the\n * daemon never depends on the framework logger directly.\n */\n readonly warn?: (message: string) => void;\n}\n\n/**\n * Stateful handle returned by {@link createTriggersDaemon}. The\n * `start()` / `stop()` methods are wired into\n * `LifecycleHooks.beforeStart` / `beforeShutdown` by `createServer`.\n *\n * @stable\n */\nexport interface TriggersDaemon {\n start(): Promise<void>;\n stop(): Promise<void>;\n status(): Promise<TriggersDaemonStatus>;\n metrics(): TriggersDaemonMetrics;\n readonly scheduler: Scheduler;\n}\n\n/**\n * @stable\n */\nexport function createTriggersDaemon(options: CreateTriggersDaemonOptions): TriggersDaemon {\n const fires = new Map<string, { success: number; error: number }>();\n let catchupApplied = 0;\n let libModeWarnings = 0;\n let lastFireAtMs: number | undefined;\n let running = false;\n let pump: Promise<void> | undefined;\n\n const warn = options.warn ?? ((m: string) => process.stderr.write(`${m}\\n`));\n\n function recordFire(id: string, kind: 'success' | 'error'): void {\n const entry = fires.get(id) ?? { success: 0, error: 0 };\n entry[kind] += 1;\n fires.set(id, entry);\n lastFireAtMs = Date.now();\n }\n\n async function pumpEvents(): Promise<void> {\n try {\n for await (const event of options.scheduler.events()) {\n observe(event);\n }\n } catch (err) {\n warn(`[graphorin/server] triggers daemon event pump aborted: ${describeError(err)}`);\n }\n }\n\n function observe(event: SchedulerEvent): void {\n switch (event.type) {\n case 'fire-end':\n recordFire(event.id, 'success');\n return;\n case 'fire-error':\n recordFire(event.id, 'error');\n warn(\n `[graphorin/server] triggers daemon: trigger '${event.id}' failed after ${event.durationMs}ms: ${describeError(event.error)}`,\n );\n return;\n case 'catchup-applied':\n catchupApplied += event.missed;\n return;\n case 'lib-mode-warning':\n libModeWarnings += 1;\n return;\n default:\n return;\n }\n }\n\n return {\n get scheduler() {\n return options.scheduler;\n },\n async start() {\n if (running) return;\n running = true;\n pump = pumpEvents();\n await options.scheduler.start();\n },\n async stop() {\n if (!running) return;\n running = false;\n await options.scheduler.stop();\n if (pump !== undefined) {\n try {\n await pump;\n } catch {\n // Best-effort during stop().\n }\n pump = undefined;\n }\n },\n async status() {\n const all = await options.scheduler.list();\n let active = 0;\n let disabled = 0;\n let deferred = 0;\n for (const t of all) {\n if (t.disabled) disabled += 1;\n else active += 1;\n if (t.missedFires > 0) deferred += 1;\n }\n const out: {\n -readonly [K in keyof TriggersDaemonStatus]?: TriggersDaemonStatus[K];\n } = {\n running,\n active,\n disabled,\n deferred,\n };\n if (lastFireAtMs !== undefined) {\n out.lastFireAt = new Date(lastFireAtMs).toISOString();\n }\n return Object.freeze(out as TriggersDaemonStatus);\n },\n metrics() {\n return Object.freeze({\n fires: new Map(fires),\n catchupApplied,\n libModeWarnings,\n });\n },\n };\n}\n\n/**\n * Resolve the catch-up policy default for triggers that did not\n * declare one explicitly. Returns `'none'` per DEC-150 (personal-\n * assistant friendly).\n *\n * @stable\n */\nexport function defaultCatchupPolicy(): CatchupPolicy {\n return 'none';\n}\n\nfunction describeError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown';\n if (err instanceof Error) return err.message;\n return String(err);\n}\n"],"mappings":";;;;AA6EA,SAAgB,qBAAqB,SAAsD;CACzF,MAAM,wBAAQ,IAAI,KAAiD;CACnE,IAAI,iBAAiB;CACrB,IAAI,kBAAkB;CACtB,IAAIA;CACJ,IAAI,UAAU;CACd,IAAIC;CAEJ,MAAM,OAAO,QAAQ,UAAU,MAAc,QAAQ,OAAO,MAAM,GAAG,EAAE,IAAI;CAE3E,SAAS,WAAW,IAAY,MAAiC;EAC/D,MAAM,QAAQ,MAAM,IAAI,GAAG,IAAI;GAAE,SAAS;GAAG,OAAO;GAAG;AACvD,QAAM,SAAS;AACf,QAAM,IAAI,IAAI,MAAM;AACpB,iBAAe,KAAK,KAAK;;CAG3B,eAAe,aAA4B;AACzC,MAAI;AACF,cAAW,MAAM,SAAS,QAAQ,UAAU,QAAQ,CAClD,SAAQ,MAAM;WAET,KAAK;AACZ,QAAK,0DAA0D,cAAc,IAAI,GAAG;;;CAIxF,SAAS,QAAQ,OAA6B;AAC5C,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,eAAW,MAAM,IAAI,UAAU;AAC/B;GACF,KAAK;AACH,eAAW,MAAM,IAAI,QAAQ;AAC7B,SACE,gDAAgD,MAAM,GAAG,iBAAiB,MAAM,WAAW,MAAM,cAAc,MAAM,MAAM,GAC5H;AACD;GACF,KAAK;AACH,sBAAkB,MAAM;AACxB;GACF,KAAK;AACH,uBAAmB;AACnB;GACF,QACE;;;AAIN,QAAO;EACL,IAAI,YAAY;AACd,UAAO,QAAQ;;EAEjB,MAAM,QAAQ;AACZ,OAAI,QAAS;AACb,aAAU;AACV,UAAO,YAAY;AACnB,SAAM,QAAQ,UAAU,OAAO;;EAEjC,MAAM,OAAO;AACX,OAAI,CAAC,QAAS;AACd,aAAU;AACV,SAAM,QAAQ,UAAU,MAAM;AAC9B,OAAI,SAAS,QAAW;AACtB,QAAI;AACF,WAAM;YACA;AAGR,WAAO;;;EAGX,MAAM,SAAS;GACb,MAAM,MAAM,MAAM,QAAQ,UAAU,MAAM;GAC1C,IAAI,SAAS;GACb,IAAI,WAAW;GACf,IAAI,WAAW;AACf,QAAK,MAAM,KAAK,KAAK;AACnB,QAAI,EAAE,SAAU,aAAY;QACvB,WAAU;AACf,QAAI,EAAE,cAAc,EAAG,aAAY;;GAErC,MAAMC,MAEF;IACF;IACA;IACA;IACA;IACD;AACD,OAAI,iBAAiB,OACnB,KAAI,aAAa,IAAI,KAAK,aAAa,CAAC,aAAa;AAEvD,UAAO,OAAO,OAAO,IAA4B;;EAEnD,UAAU;AACR,UAAO,OAAO,OAAO;IACnB,OAAO,IAAI,IAAI,MAAM;IACrB;IACA;IACD,CAAC;;EAEL;;;;;;;;;AAUH,SAAgB,uBAAsC;AACpD,QAAO;;AAGT,SAAS,cAAc,KAAsB;AAC3C,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,KAAI,eAAe,MAAO,QAAO,IAAI;AACrC,QAAO,OAAO,IAAI"}
1
+ {"version":3,"file":"daemon.js","names":["lastFireAtMs: number | undefined","pump: Promise<void> | undefined","out: {\n -readonly [K in keyof TriggersDaemonStatus]?: TriggersDaemonStatus[K];\n }"],"sources":["../../src/triggers/daemon.ts"],"sourcesContent":["/**\n * Trigger scheduler integration with the server lifecycle.\n *\n * The daemon wraps a `Scheduler` from `@graphorin/triggers` and binds\n * its `start()` / `stop()` calls to the `beforeStart` / `beforeShutdown`\n * lifecycle hooks. On `start()`, every persisted trigger is loaded\n * from `trigger_state` (durable) and the per-trigger `catchupPolicy`\n * is honoured so cron / interval / idle / event triggers survive\n * process restarts as documented in the runtime spec.\n *\n * The daemon never invokes user callbacks itself - those run inside\n * the `Scheduler`. The daemon only owns lifecycle + observability.\n *\n * @packageDocumentation\n */\n\nimport type { CatchupPolicy, Scheduler, SchedulerEvent } from '@graphorin/triggers';\n\n/**\n * Snapshot exposed via {@link TriggersDaemon.status} + the\n * `/v1/health` aggregator. Counts split by `disabled` so the health\n * endpoint can flag a deployment that disabled every cron trigger.\n *\n * @stable\n */\nexport interface TriggersDaemonStatus {\n readonly running: boolean;\n readonly active: number;\n readonly disabled: number;\n readonly deferred: number;\n /** Last fire timestamp across the entire pool (ISO-8601). */\n readonly lastFireAt?: string;\n}\n\n/**\n * Aggregate counters surfaced through the Prometheus\n * `/v1/metrics` exposition. Updated incrementally as the scheduler\n * publishes lifecycle events.\n *\n * @stable\n */\nexport interface TriggersDaemonMetrics {\n readonly fires: ReadonlyMap<string, { readonly success: number; readonly error: number }>;\n readonly catchupApplied: number;\n readonly libModeWarnings: number;\n}\n\n/**\n * @stable\n */\nexport interface CreateTriggersDaemonOptions {\n readonly scheduler: Scheduler;\n /**\n * Optional logger. Defaults to the standard error stream so the\n * daemon never depends on the framework logger directly.\n */\n readonly warn?: (message: string) => void;\n}\n\n/**\n * Stateful handle returned by {@link createTriggersDaemon}. The\n * `start()` / `stop()` methods are wired into\n * `LifecycleHooks.beforeStart` / `beforeShutdown` by `createServer`.\n *\n * @stable\n */\nexport interface TriggersDaemon {\n start(): Promise<void>;\n stop(): Promise<void>;\n status(): Promise<TriggersDaemonStatus>;\n metrics(): TriggersDaemonMetrics;\n readonly scheduler: Scheduler;\n}\n\n/**\n * @stable\n */\nexport function createTriggersDaemon(options: CreateTriggersDaemonOptions): TriggersDaemon {\n const fires = new Map<string, { success: number; error: number }>();\n let catchupApplied = 0;\n let libModeWarnings = 0;\n let lastFireAtMs: number | undefined;\n let running = false;\n let pump: Promise<void> | undefined;\n\n const warn = options.warn ?? ((m: string) => process.stderr.write(`${m}\\n`));\n\n function recordFire(id: string, kind: 'success' | 'error'): void {\n const entry = fires.get(id) ?? { success: 0, error: 0 };\n entry[kind] += 1;\n fires.set(id, entry);\n lastFireAtMs = Date.now();\n }\n\n async function pumpEvents(): Promise<void> {\n try {\n for await (const event of options.scheduler.events()) {\n observe(event);\n }\n } catch (err) {\n warn(`[graphorin/server] triggers daemon event pump aborted: ${describeError(err)}`);\n }\n }\n\n function observe(event: SchedulerEvent): void {\n switch (event.type) {\n case 'fire-end':\n recordFire(event.id, 'success');\n return;\n case 'fire-error':\n recordFire(event.id, 'error');\n warn(\n `[graphorin/server] triggers daemon: trigger '${event.id}' failed after ${event.durationMs}ms: ${describeError(event.error)}`,\n );\n return;\n case 'catchup-applied':\n catchupApplied += event.missed;\n return;\n case 'lib-mode-warning':\n libModeWarnings += 1;\n return;\n default:\n return;\n }\n }\n\n return {\n get scheduler() {\n return options.scheduler;\n },\n async start() {\n if (running) return;\n running = true;\n pump = pumpEvents();\n await options.scheduler.start();\n },\n async stop() {\n if (!running) return;\n running = false;\n await options.scheduler.stop();\n if (pump !== undefined) {\n try {\n await pump;\n } catch {\n // Best-effort during stop().\n }\n pump = undefined;\n }\n },\n async status() {\n const all = await options.scheduler.list();\n let active = 0;\n let disabled = 0;\n let deferred = 0;\n for (const t of all) {\n if (t.disabled) disabled += 1;\n else active += 1;\n if (t.missedFires > 0) deferred += 1;\n }\n const out: {\n -readonly [K in keyof TriggersDaemonStatus]?: TriggersDaemonStatus[K];\n } = {\n running,\n active,\n disabled,\n deferred,\n };\n if (lastFireAtMs !== undefined) {\n out.lastFireAt = new Date(lastFireAtMs).toISOString();\n }\n return Object.freeze(out as TriggersDaemonStatus);\n },\n metrics() {\n return Object.freeze({\n fires: new Map(fires),\n catchupApplied,\n libModeWarnings,\n });\n },\n };\n}\n\n/**\n * Resolve the catch-up policy default for triggers that did not\n * declare one explicitly. Returns `'none'` per DEC-150 (personal-\n * assistant friendly).\n *\n * @stable\n */\nexport function defaultCatchupPolicy(): CatchupPolicy {\n return 'none';\n}\n\nfunction describeError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown';\n if (err instanceof Error) return err.message;\n return String(err);\n}\n"],"mappings":";;;;AA6EA,SAAgB,qBAAqB,SAAsD;CACzF,MAAM,wBAAQ,IAAI,KAAiD;CACnE,IAAI,iBAAiB;CACrB,IAAI,kBAAkB;CACtB,IAAIA;CACJ,IAAI,UAAU;CACd,IAAIC;CAEJ,MAAM,OAAO,QAAQ,UAAU,MAAc,QAAQ,OAAO,MAAM,GAAG,EAAE,IAAI;CAE3E,SAAS,WAAW,IAAY,MAAiC;EAC/D,MAAM,QAAQ,MAAM,IAAI,GAAG,IAAI;GAAE,SAAS;GAAG,OAAO;GAAG;AACvD,QAAM,SAAS;AACf,QAAM,IAAI,IAAI,MAAM;AACpB,iBAAe,KAAK,KAAK;;CAG3B,eAAe,aAA4B;AACzC,MAAI;AACF,cAAW,MAAM,SAAS,QAAQ,UAAU,QAAQ,CAClD,SAAQ,MAAM;WAET,KAAK;AACZ,QAAK,0DAA0D,cAAc,IAAI,GAAG;;;CAIxF,SAAS,QAAQ,OAA6B;AAC5C,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,eAAW,MAAM,IAAI,UAAU;AAC/B;GACF,KAAK;AACH,eAAW,MAAM,IAAI,QAAQ;AAC7B,SACE,gDAAgD,MAAM,GAAG,iBAAiB,MAAM,WAAW,MAAM,cAAc,MAAM,MAAM,GAC5H;AACD;GACF,KAAK;AACH,sBAAkB,MAAM;AACxB;GACF,KAAK;AACH,uBAAmB;AACnB;GACF,QACE;;;AAIN,QAAO;EACL,IAAI,YAAY;AACd,UAAO,QAAQ;;EAEjB,MAAM,QAAQ;AACZ,OAAI,QAAS;AACb,aAAU;AACV,UAAO,YAAY;AACnB,SAAM,QAAQ,UAAU,OAAO;;EAEjC,MAAM,OAAO;AACX,OAAI,CAAC,QAAS;AACd,aAAU;AACV,SAAM,QAAQ,UAAU,MAAM;AAC9B,OAAI,SAAS,QAAW;AACtB,QAAI;AACF,WAAM;YACA;AAGR,WAAO;;;EAGX,MAAM,SAAS;GACb,MAAM,MAAM,MAAM,QAAQ,UAAU,MAAM;GAC1C,IAAI,SAAS;GACb,IAAI,WAAW;GACf,IAAI,WAAW;AACf,QAAK,MAAM,KAAK,KAAK;AACnB,QAAI,EAAE,SAAU,aAAY;QACvB,WAAU;AACf,QAAI,EAAE,cAAc,EAAG,aAAY;;GAErC,MAAMC,MAEF;IACF;IACA;IACA;IACA;IACD;AACD,OAAI,iBAAiB,OACnB,KAAI,aAAa,IAAI,KAAK,aAAa,CAAC,aAAa;AAEvD,UAAO,OAAO,OAAO,IAA4B;;EAEnD,UAAU;AACR,UAAO,OAAO,OAAO;IACnB,OAAO,IAAI,IAAI,MAAM;IACrB;IACA;IACD,CAAC;;EAEL;;;;;;;;;AAUH,SAAgB,uBAAsC;AACpD,QAAO;;AAGT,SAAS,cAAc,KAAsB;AAC3C,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,KAAI,eAAe,MAAO,QAAO,IAAI;AACrC,QAAO,OAAO,IAAI"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":["scheduler: Scheduler","removedIds: string[]"],"sources":["../../src/triggers/routes.ts"],"sourcesContent":["/**\n * REST routes for the `/v1/triggers` family. Mounted by the server\n * factory when a {@link TriggersDaemon} is wired in.\n *\n * GET /triggers (scope `triggers:read`)\n * GET /triggers/:id (scope `triggers:read`)\n * POST /triggers/:id/fire (scope `triggers:fire`)\n * POST /triggers/:id/disable (scope `triggers:disable`) flag flip\n * POST /triggers/:id/enable (scope `triggers:disable`) flag flip\n * DELETE /triggers/:id (scope `triggers:disable`) unregister\n * POST /triggers/prune (scope `triggers:disable`)\n *\n * Response shapes mirror the persisted `TriggerState` rows so CLI +\n * dashboard consumers can render them without re-mapping.\n *\n * @packageDocumentation\n */\n\nimport type { TriggerState } from '@graphorin/core/contracts';\nimport type { Scheduler } from '@graphorin/triggers';\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { TriggersDaemon } from './daemon.js';\n\n/**\n * @stable\n */\nexport interface TriggersRoutesDeps {\n readonly daemon: TriggersDaemon;\n}\n\n/**\n * @stable\n */\nexport function createTriggersRoutes(\n deps: TriggersRoutesDeps,\n): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const scheduler: Scheduler = deps.daemon.scheduler;\n\n app.get('/', createScopeMiddleware('triggers:read'), async (c) => {\n const all = await scheduler.list();\n return c.json({ triggers: all.map(serializeTrigger) });\n });\n\n app.get('/:id', createScopeMiddleware('triggers:read'), async (c) => {\n const id = c.req.param('id');\n const all = await scheduler.list();\n const found = all.find((t) => t.id === id);\n if (found === undefined) {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n return c.json({ trigger: serializeTrigger(found) });\n });\n\n app.post('/:id/fire', createScopeMiddleware('triggers:fire'), async (c) => {\n const id = c.req.param('id');\n try {\n await scheduler.fire(id);\n } catch (err) {\n return c.json(\n {\n error: 'not-found',\n message: err instanceof Error ? err.message : `Trigger '${id}' not found.`,\n },\n 404,\n );\n }\n return c.json({ ok: true, fired: id });\n });\n\n // IP-17: disable/enable are non-destructive flag flips on the\n // persistent TriggerState (in agreement with the CLI); the\n // destructive removal is DELETE /:id.\n app.post('/:id/disable', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n try {\n const updated = await scheduler.setDisabled(id, true);\n return c.json({ ok: true, trigger: serializeTrigger(updated) });\n } catch {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n });\n\n app.post('/:id/enable', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n try {\n const updated = await scheduler.setDisabled(id, false);\n return c.json({ ok: true, trigger: serializeTrigger(updated) });\n } catch {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n });\n\n app.delete('/:id', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n const all = await scheduler.list();\n if (!all.some((t) => t.id === id)) {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n await scheduler.unregister(id);\n return c.json({ ok: true, removed: id });\n });\n\n app.post('/prune', createScopeMiddleware('triggers:disable'), async (c) => {\n const all = await scheduler.list();\n const removedIds: string[] = [];\n for (const trigger of all) {\n if (trigger.disabled) {\n await scheduler.unregister(trigger.id);\n removedIds.push(trigger.id);\n }\n }\n return c.json({ ok: true, removed: removedIds, count: removedIds.length });\n });\n\n return app;\n}\n\nfunction serializeTrigger(state: TriggerState): Record<string, unknown> {\n return {\n id: state.id,\n kind: state.kind,\n spec: state.spec,\n callbackRef: state.callbackRef,\n ...(state.nextFireAt !== undefined ? { nextFireAt: state.nextFireAt } : {}),\n ...(state.lastFiredAt !== undefined ? { lastFiredAt: state.lastFiredAt } : {}),\n missedFires: state.missedFires,\n disabled: state.disabled,\n catchupPolicy: state.catchupPolicy,\n maxCatchupRuns: state.maxCatchupRuns,\n catchupWindowMs: state.catchupWindowMs,\n ...(state.tags !== undefined ? { tags: [...state.tags] } : {}),\n createdAt: state.createdAt,\n ...(state.updatedAt !== undefined ? { updatedAt: state.updatedAt } : {}),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","names":["scheduler: Scheduler","removedIds: string[]"],"sources":["../../src/triggers/routes.ts"],"sourcesContent":["/**\n * REST routes for the `/v1/triggers` family. Mounted by the server\n * factory when a {@link TriggersDaemon} is wired in.\n *\n * GET /triggers (scope `triggers:read`)\n * GET /triggers/:id (scope `triggers:read`)\n * POST /triggers/:id/fire (scope `triggers:fire`)\n * POST /triggers/:id/disable (scope `triggers:disable`) - flag flip\n * POST /triggers/:id/enable (scope `triggers:disable`) - flag flip\n * DELETE /triggers/:id (scope `triggers:disable`) - unregister\n * POST /triggers/prune (scope `triggers:disable`)\n *\n * Response shapes mirror the persisted `TriggerState` rows so CLI +\n * dashboard consumers can render them without re-mapping.\n *\n * @packageDocumentation\n */\n\nimport type { TriggerState } from '@graphorin/core/contracts';\nimport type { Scheduler } from '@graphorin/triggers';\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { TriggersDaemon } from './daemon.js';\n\n/**\n * @stable\n */\nexport interface TriggersRoutesDeps {\n readonly daemon: TriggersDaemon;\n}\n\n/**\n * @stable\n */\nexport function createTriggersRoutes(\n deps: TriggersRoutesDeps,\n): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const scheduler: Scheduler = deps.daemon.scheduler;\n\n app.get('/', createScopeMiddleware('triggers:read'), async (c) => {\n const all = await scheduler.list();\n return c.json({ triggers: all.map(serializeTrigger) });\n });\n\n app.get('/:id', createScopeMiddleware('triggers:read'), async (c) => {\n const id = c.req.param('id');\n const all = await scheduler.list();\n const found = all.find((t) => t.id === id);\n if (found === undefined) {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n return c.json({ trigger: serializeTrigger(found) });\n });\n\n app.post('/:id/fire', createScopeMiddleware('triggers:fire'), async (c) => {\n const id = c.req.param('id');\n try {\n await scheduler.fire(id);\n } catch (err) {\n return c.json(\n {\n error: 'not-found',\n message: err instanceof Error ? err.message : `Trigger '${id}' not found.`,\n },\n 404,\n );\n }\n return c.json({ ok: true, fired: id });\n });\n\n // IP-17: disable/enable are non-destructive flag flips on the\n // persistent TriggerState (in agreement with the CLI); the\n // destructive removal is DELETE /:id.\n app.post('/:id/disable', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n try {\n const updated = await scheduler.setDisabled(id, true);\n return c.json({ ok: true, trigger: serializeTrigger(updated) });\n } catch {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n });\n\n app.post('/:id/enable', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n try {\n const updated = await scheduler.setDisabled(id, false);\n return c.json({ ok: true, trigger: serializeTrigger(updated) });\n } catch {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n });\n\n app.delete('/:id', createScopeMiddleware('triggers:disable'), async (c) => {\n const id = c.req.param('id');\n const all = await scheduler.list();\n if (!all.some((t) => t.id === id)) {\n return c.json({ error: 'not-found', message: `Trigger '${id}' not found.` }, 404);\n }\n await scheduler.unregister(id);\n return c.json({ ok: true, removed: id });\n });\n\n app.post('/prune', createScopeMiddleware('triggers:disable'), async (c) => {\n const all = await scheduler.list();\n const removedIds: string[] = [];\n for (const trigger of all) {\n if (trigger.disabled) {\n await scheduler.unregister(trigger.id);\n removedIds.push(trigger.id);\n }\n }\n return c.json({ ok: true, removed: removedIds, count: removedIds.length });\n });\n\n return app;\n}\n\nfunction serializeTrigger(state: TriggerState): Record<string, unknown> {\n return {\n id: state.id,\n kind: state.kind,\n spec: state.spec,\n callbackRef: state.callbackRef,\n ...(state.nextFireAt !== undefined ? { nextFireAt: state.nextFireAt } : {}),\n ...(state.lastFiredAt !== undefined ? { lastFiredAt: state.lastFiredAt } : {}),\n missedFires: state.missedFires,\n disabled: state.disabled,\n catchupPolicy: state.catchupPolicy,\n maxCatchupRuns: state.maxCatchupRuns,\n catchupWindowMs: state.catchupWindowMs,\n ...(state.tags !== undefined ? { tags: [...state.tags] } : {}),\n createdAt: state.createdAt,\n ...(state.updatedAt !== undefined ? { updatedAt: state.updatedAt } : {}),\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","names":[],"sources":["../../src/ws/dispatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA0EA;;;;;AA0BiB,UAhEA,mBAAA,CAgEsB;EAe3B,SAAA,UAAe,CAAA,EA9EH,wBAiFK;EAkBZ,SAAA,YAAY,CAAA,EAlGH,mBAkGG;EACP;;;;EAyBS,SAAA,uBAAA,CAAA,EAAA,MAAA;EAInB,SAAA,GAAA,CAAA,EAAA,GAAA,GAAA,MAAA;EAI6C;;;;AAazD;EA6BgB,SAAA,MAAA,CAAA,EAAA,CAAA,KAAkB,EAlKN,mBAkKgB,EAAA,GAAA,IAAA;;;;;;;KA1JhC,mBAAA;;;mBAIW;;;;;;;;;;;;;;;;UAYN,kBAAA;;;0BAGS,cAAc;;cAE1B;;;;;;;;;;;;;;;;;;;;UAqBG,sBAAA;;;wBAGO;;;;;;;;;;;KAYZ,eAAA;;yBAGiB;;;;;;;;;;;;UAkBZ,YAAA;sBACK;yBACG;;;;;6BAKI;;;;;;;;;;;;;;MAYvB;;;;;;;+BAOyB;;gDAInB;;2CAI+B,cAAc;gDACT;;;;;;;;;;;;;;UAY/B,cAAA;;;;;;;;;;iBA6BD,kBAAA,WAA4B,sBAA2B"}
1
+ {"version":3,"file":"dispatcher.d.ts","names":[],"sources":["../../src/ws/dispatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA0EA;;;;;AA0BiB,UAhEA,mBAAA,CAgEsB;EAe3B,SAAA,UAAe,CAAA,EA9EH,wBAiFK;EAkBZ,SAAA,YAAY,CAAA,EAlGH,mBAkGG;EACP;;;;EAyBS,SAAA,uBAAA,CAAA,EAAA,MAAA;EAInB,SAAA,GAAA,CAAA,EAAA,GAAA,GAAA,MAAA;EAI6C;;;;AAazD;EA6BgB,SAAA,MAAA,CAAA,EAAA,CAAA,KAAkB,EAlKN,mBAkKgB,EAAA,GAAA,IAA2B;;;;;;;KA1J3D,mBAAA;;;mBAIW;;;;;;;;;;;;;;;;UAYN,kBAAA;;;0BAGS,cAAc;;cAE1B;;;;;;;;;;;;;;;;;;;;UAqBG,sBAAA;;;wBAGO;;;;;;;;;;;KAYZ,eAAA;;yBAGiB;;;;;;;;;;;;UAkBZ,YAAA;sBACK;yBACG;;;;;6BAKI;;;;;;;;;;;;;;MAYvB;;;;;;;+BAOyB;;gDAInB;;2CAI+B,cAAc;gDACT;;;;;;;;;;;;;;UAY/B,cAAA;;;;;;;;;;iBA6BD,kBAAA,WAA4B,sBAA2B"}
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.js","names":["record: SubscriptionRecord","snapshotEventId: string | undefined","frame: ServerEventFrame","bufferedFrame: ServerEventFrame"],"sources":["../../src/ws/dispatcher.ts"],"sourcesContent":["/**\n * Per-server WebSocket dispatcher. Owns the in-memory subscription\n * map, fans out events to matching connections, applies the\n * delivery-layer commentary-phase trace sanitization, validates\n * outbound frames against the `@graphorin/protocol` schema, and\n * exposes a tiny `emit(subject, event)` surface route handlers\n * (and the agent / workflow runtimes) consume.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n type ServerEventFrame,\n type ServerLifecycleFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport {\n createReplayBuffer,\n type ReplayBuffer,\n type ReplayBufferOptions,\n} from './replay-buffer.js';\nimport { isSubjectAllowed, type ParsedSubject, tryParseSubject } from './subjects.js';\n\n/**\n * Public configuration accepted by {@link createWsDispatcher}.\n *\n * @stable\n */\nexport interface WsDispatcherOptions {\n readonly commentary?: DeliveryCommentaryConfig;\n readonly replayBuffer?: ReplayBufferOptions;\n /**\n * Cap on outstanding events queued for an offline subscriber.\n * Defaults to the same value as the replay buffer per-subject cap.\n */\n readonly perConnectionQueueLimit?: number;\n readonly now?: () => number;\n /**\n * Logger sink for protocol violations + dropped frames. When\n * omitted, the dispatcher is silent (production wiring uses the\n * server's structured logger).\n */\n readonly onWarn?: (event: WsDispatcherWarning) => void;\n}\n\n/**\n * Discriminator surfaced to the optional warn sink.\n *\n * @stable\n */\nexport type WsDispatcherWarning =\n | {\n readonly kind: 'invalid-frame';\n readonly subscriptionId: string;\n readonly issues: ReadonlyArray<string>;\n }\n | { readonly kind: 'queue-overflow'; readonly subscriptionId: string }\n | { readonly kind: 'subject-rejected'; readonly subject: string; readonly reason: string };\n\n/**\n * Subscriber surface used by the dispatcher. Each WebSocket\n * connection wraps its `WSContext.send` in this interface so the\n * dispatcher does not depend on `@hono/node-ws` types directly.\n *\n * @stable\n */\nexport interface WsSubscriberHandle {\n readonly id: string;\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n /** Send a server frame; the dispatcher already validated it. */\n send(frame: ServerMessage): void;\n /** Close the underlying WebSocket with a Graphorin close code. */\n close(code: number, reason: string): void;\n /**\n * Optional buffered-byte sample. The dispatcher reads this on every\n * emit to detect sustained backpressure and close the connection\n * with the Graphorin `flow.throttled` code (4006) before the OS-\n * level send buffer collapses. Consumers backed by `@hono/node-ws`\n * can return the underlying `ws.bufferedAmount`. When the field is\n * not implemented, the dispatcher falls back to the per-connection\n * outstanding-event counter.\n */\n bufferedAmount?(): number;\n}\n\n/**\n * Snapshot of an active subscription. Surfaced via\n * {@link WsDispatcher.snapshotSubscription}.\n *\n * @stable\n */\nexport interface WsSubscriptionSnapshot {\n readonly subscriptionId: string;\n readonly subject: string;\n readonly subjectKind: ParsedSubject['kind'];\n readonly subscriberId: string;\n readonly tokenId: string;\n readonly createdAt: number;\n readonly lastEventId: string | undefined;\n}\n\n/**\n * Result of {@link WsDispatcher.subscribe}.\n *\n * @stable\n */\nexport type SubscribeResult =\n | {\n readonly ok: true;\n readonly subscription: WsSubscriptionSnapshot;\n readonly replayedCount: number;\n readonly snapshotEventId: string | undefined;\n }\n | {\n readonly ok: false;\n readonly reason:\n | 'subject-malformed'\n | 'subject-unknown'\n | 'subject-wildcard'\n | 'scope-denied';\n };\n\n/**\n * Public surface of the dispatcher.\n *\n * @stable\n */\nexport interface WsDispatcher {\n readonly sanitizer: DeliveryCommentarySanitizer;\n readonly replayBuffer: ReplayBuffer;\n /**\n * Register a subscriber (one per WebSocket connection).\n * `unregister` is called on close.\n */\n registerSubscriber(handle: WsSubscriberHandle): { unregister(): void };\n /**\n * Open a new subscription for an active subscriber. Returns either\n * the subscription snapshot + replayed-event count or a typed\n * failure reason the caller maps to the appropriate close code /\n * RPC error.\n */\n subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult;\n unsubscribe(subscriptionId: string): boolean;\n /**\n * Push an event into the dispatcher. Goes through the sanitizer,\n * validates against the protocol schema, persists into the replay\n * buffer, and fans out to every matching active subscription.\n */\n emit(subject: string, frame: BareEventFrame): void;\n /** Push a lifecycle frame to a single subscription. */\n emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void;\n /** List active subscriptions for a given subscriber (diagnostics). */\n listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot>;\n snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined;\n size(): { readonly subscribers: number; readonly subscriptions: number };\n /** Clear in-memory state (used during graceful shutdown). */\n shutdown(): void;\n}\n\n/**\n * Frame argument accepted by {@link WsDispatcher.emit}. The\n * dispatcher fills in `subscriptionId`, `subject`, and `eventId`.\n *\n * @stable\n */\nexport interface BareEventFrame {\n readonly type: string;\n readonly payload: unknown;\n readonly eventId?: string;\n}\n\ninterface SubscriberRecord {\n readonly handle: WsSubscriberHandle;\n readonly subscriptions: Set<string>;\n outstandingEvents: number;\n closed: boolean;\n}\n\ninterface SubscriptionRecord {\n readonly subscriberId: string;\n /** IP-18: the authenticating principal's token id (not the connection id). */\n readonly tokenId: string;\n readonly subscriptionId: string;\n readonly subject: string;\n readonly parsed: ParsedSubject;\n readonly createdAt: number;\n lastEventId: string | undefined;\n}\n\n/**\n * Build a fresh dispatcher.\n *\n * @stable\n */\nexport function createWsDispatcher(options: WsDispatcherOptions = {}): WsDispatcher {\n const sanitizer = createDeliveryCommentarySanitizer(options.commentary);\n const replayBuffer = createReplayBuffer(options.replayBuffer ?? {});\n const now = options.now ?? Date.now;\n const onWarn = options.onWarn;\n const perConnectionQueueLimit = options.perConnectionQueueLimit ?? 1_000;\n const subscribers = new Map<string, SubscriberRecord>();\n const subscriptions = new Map<string, SubscriptionRecord>();\n const subjectIndex = new Map<string, Set<string>>();\n let eventCounter = 0;\n\n function indexSubject(subject: string, subscriptionId: string): void {\n let set = subjectIndex.get(subject);\n if (set === undefined) {\n set = new Set();\n subjectIndex.set(subject, set);\n }\n set.add(subscriptionId);\n }\n\n function deindexSubject(subject: string, subscriptionId: string): void {\n const set = subjectIndex.get(subject);\n if (set === undefined) return;\n set.delete(subscriptionId);\n if (set.size === 0) subjectIndex.delete(subject);\n }\n\n function nextEventId(): string {\n eventCounter += 1;\n return `evt-${now().toString(36)}-${eventCounter.toString(36)}`;\n }\n\n function dispatchToSubscriber(record: SubscriptionRecord, frame: ServerMessage): void {\n const subscriber = subscribers.get(record.subscriberId);\n if (subscriber === undefined || subscriber.closed) return;\n // Per-connection backpressure: track the outstanding-event counter\n // and consult the optional `bufferedAmount()` sample. The\n // dispatcher closes the connection with the Graphorin\n // `flow.throttled` code (4006) when either signal exceeds the\n // configured limit so the client can reconnect against the\n // replay buffer instead of accumulating an unbounded send queue.\n const buffered =\n typeof subscriber.handle.bufferedAmount === 'function'\n ? subscriber.handle.bufferedAmount()\n : 0;\n if (\n subscriber.outstandingEvents >= perConnectionQueueLimit ||\n buffered >= perConnectionQueueLimit * 1024\n ) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n return;\n }\n subscriber.outstandingEvents += 1;\n try {\n subscriber.handle.send(frame);\n } catch (err) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n void err;\n } finally {\n subscriber.outstandingEvents = Math.max(0, subscriber.outstandingEvents - 1);\n }\n }\n\n function shutdownSubscriber(subscriber: SubscriberRecord, code: number, reason: string): void {\n if (subscriber.closed) return;\n subscriber.closed = true;\n try {\n subscriber.handle.close(code, reason);\n } catch {\n // ignore — we are tearing the subscriber down.\n }\n for (const id of subscriber.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscriber.subscriptions.clear();\n subscribers.delete(subscriber.handle.id);\n }\n\n function validateAndDispatch(record: SubscriptionRecord, frame: ServerMessage): void {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) {\n onWarn?.({\n kind: 'invalid-frame',\n subscriptionId: record.subscriptionId,\n issues: validated.error.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`),\n });\n return;\n }\n dispatchToSubscriber(record, validated.data);\n }\n\n function registerSubscriber(handle: WsSubscriberHandle): { unregister(): void } {\n subscribers.set(handle.id, {\n handle,\n subscriptions: new Set(),\n outstandingEvents: 0,\n closed: false,\n });\n return {\n unregister: () => {\n const record = subscribers.get(handle.id);\n if (record === undefined) return;\n for (const id of record.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscribers.delete(handle.id);\n },\n };\n }\n\n function subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult {\n const subscriber = subscribers.get(input.subscriberId);\n if (subscriber === undefined) {\n return { ok: false, reason: 'subject-unknown' };\n }\n const parseResult = tryParseSubject(input.subject);\n if (!parseResult.ok) {\n const reason = parseResult.reason;\n onWarn?.({ kind: 'subject-rejected', subject: input.subject, reason });\n if (reason === 'wildcard-not-supported') return { ok: false, reason: 'subject-wildcard' };\n if (reason === 'unknown-subject') return { ok: false, reason: 'subject-unknown' };\n return { ok: false, reason: 'subject-malformed' };\n }\n if (!isSubjectAllowed(subscriber.handle.grantedScopes, parseResult.subject)) {\n return { ok: false, reason: 'scope-denied' };\n }\n const record: SubscriptionRecord = {\n subscriberId: input.subscriberId,\n tokenId: subscriber.handle.tokenId,\n subscriptionId: input.subscriptionId,\n subject: input.subject,\n parsed: parseResult.subject,\n createdAt: now(),\n lastEventId: undefined,\n };\n subscriptions.set(input.subscriptionId, record);\n subscriber.subscriptions.add(input.subscriptionId);\n indexSubject(input.subject, input.subscriptionId);\n const replay = replayBuffer.replay(input.subject, input.sinceEventId);\n let replayed = 0;\n let snapshotEventId: string | undefined = replay.nextEventIdHint;\n for (const event of replay.events) {\n const frame: ServerEventFrame = {\n ...event,\n subscriptionId: input.subscriptionId,\n };\n validateAndDispatch(record, frame);\n record.lastEventId = frame.eventId;\n replayed += 1;\n }\n if (replay.droppedCount > 0) {\n validateAndDispatch(record, {\n v: '1',\n kind: 'replay-marker',\n subscriptionId: input.subscriptionId,\n eventId: snapshotEventId ?? `evt-replay-${now().toString(36)}`,\n droppedCount: replay.droppedCount,\n note: `Replay buffer dropped ${replay.droppedCount} events before resume.`,\n });\n snapshotEventId = snapshotEventId ?? `evt-replay-${now().toString(36)}`;\n }\n return {\n ok: true,\n subscription: snapshotForRecord(record),\n replayedCount: replayed,\n snapshotEventId,\n };\n }\n\n function unsubscribe(subscriptionId: string): boolean {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return false;\n const subscriber = subscribers.get(record.subscriberId);\n subscriber?.subscriptions.delete(subscriptionId);\n deindexSubject(record.subject, subscriptionId);\n subscriptions.delete(subscriptionId);\n return true;\n }\n\n function emit(subject: string, bare: BareEventFrame): void {\n const eventId = bare.eventId ?? nextEventId();\n // Buffer first so the replay layer captures every event, even\n // when there is no live subscriber. The buffered frame is\n // sanitizer-neutral (the per-subscription dispatch sanitizes\n // again on the wire so the second pass is idempotent — by\n // construction matching wraps are not re-wrapped).\n const bufferedFrame: ServerEventFrame = {\n v: '1',\n kind: 'event',\n eventId,\n subscriptionId: '__pending__',\n subject,\n type: bare.type,\n payload: bare.payload,\n };\n replayBuffer.push(subject, bufferedFrame);\n const subjectSubscribers = subjectIndex.get(subject);\n if (subjectSubscribers === undefined || subjectSubscribers.size === 0) return;\n for (const subscriptionId of subjectSubscribers) {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) continue;\n const sanitized = sanitizer.sanitize({ ...bufferedFrame, subscriptionId }, 'ws');\n validateAndDispatch(record, sanitized);\n record.lastEventId = sanitized.eventId;\n if (sanitized.eventId === eventId) continue;\n }\n void isEventFrame; // keep import warm\n }\n\n function emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return;\n const frame: ServerLifecycleFrame = {\n v: '1',\n kind: 'lifecycle',\n subscriptionId,\n status,\n ...(reason !== undefined ? { reason } : {}),\n };\n validateAndDispatch(record, frame);\n }\n\n function listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot> {\n const subscriber = subscribers.get(subscriberId);\n if (subscriber === undefined) return Object.freeze([]);\n return Object.freeze(\n [...subscriber.subscriptions]\n .map((id) => subscriptions.get(id))\n .filter((entry): entry is SubscriptionRecord => entry !== undefined)\n .map(snapshotForRecord),\n );\n }\n\n function snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return undefined;\n return snapshotForRecord(record);\n }\n\n function size(): { readonly subscribers: number; readonly subscriptions: number } {\n return Object.freeze({\n subscribers: subscribers.size,\n subscriptions: subscriptions.size,\n });\n }\n\n function shutdown(): void {\n for (const subscription of subscriptions.values()) {\n const subscriber = subscribers.get(subscription.subscriberId);\n try {\n subscriber?.handle.send({\n v: '1',\n kind: 'lifecycle',\n subscriptionId: subscription.subscriptionId,\n status: 'aborted',\n reason: 'server-shutdown',\n });\n } catch {\n // swallow — we are tearing down.\n }\n }\n subscriptions.clear();\n subjectIndex.clear();\n subscribers.clear();\n }\n\n return Object.freeze({\n sanitizer,\n replayBuffer,\n registerSubscriber,\n subscribe,\n unsubscribe,\n emit,\n emitLifecycle,\n listForSubscriber,\n snapshotSubscription,\n size,\n shutdown,\n });\n}\n\nfunction snapshotForRecord(record: SubscriptionRecord): WsSubscriptionSnapshot {\n return Object.freeze({\n subscriptionId: record.subscriptionId,\n subject: record.subject,\n subjectKind: record.parsed.kind,\n subscriberId: record.subscriberId,\n tokenId: record.tokenId,\n createdAt: record.createdAt,\n lastEventId: record.lastEventId,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dispatcher.js","names":["record: SubscriptionRecord","snapshotEventId: string | undefined","frame: ServerEventFrame","bufferedFrame: ServerEventFrame"],"sources":["../../src/ws/dispatcher.ts"],"sourcesContent":["/**\n * Per-server WebSocket dispatcher. Owns the in-memory subscription\n * map, fans out events to matching connections, applies the\n * delivery-layer commentary-phase trace sanitization, validates\n * outbound frames against the `@graphorin/protocol` schema, and\n * exposes a tiny `emit(subject, event)` surface route handlers\n * (and the agent / workflow runtimes) consume.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n type ServerEventFrame,\n type ServerLifecycleFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport {\n createReplayBuffer,\n type ReplayBuffer,\n type ReplayBufferOptions,\n} from './replay-buffer.js';\nimport { isSubjectAllowed, type ParsedSubject, tryParseSubject } from './subjects.js';\n\n/**\n * Public configuration accepted by {@link createWsDispatcher}.\n *\n * @stable\n */\nexport interface WsDispatcherOptions {\n readonly commentary?: DeliveryCommentaryConfig;\n readonly replayBuffer?: ReplayBufferOptions;\n /**\n * Cap on outstanding events queued for an offline subscriber.\n * Defaults to the same value as the replay buffer per-subject cap.\n */\n readonly perConnectionQueueLimit?: number;\n readonly now?: () => number;\n /**\n * Logger sink for protocol violations + dropped frames. When\n * omitted, the dispatcher is silent (production wiring uses the\n * server's structured logger).\n */\n readonly onWarn?: (event: WsDispatcherWarning) => void;\n}\n\n/**\n * Discriminator surfaced to the optional warn sink.\n *\n * @stable\n */\nexport type WsDispatcherWarning =\n | {\n readonly kind: 'invalid-frame';\n readonly subscriptionId: string;\n readonly issues: ReadonlyArray<string>;\n }\n | { readonly kind: 'queue-overflow'; readonly subscriptionId: string }\n | { readonly kind: 'subject-rejected'; readonly subject: string; readonly reason: string };\n\n/**\n * Subscriber surface used by the dispatcher. Each WebSocket\n * connection wraps its `WSContext.send` in this interface so the\n * dispatcher does not depend on `@hono/node-ws` types directly.\n *\n * @stable\n */\nexport interface WsSubscriberHandle {\n readonly id: string;\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n /** Send a server frame; the dispatcher already validated it. */\n send(frame: ServerMessage): void;\n /** Close the underlying WebSocket with a Graphorin close code. */\n close(code: number, reason: string): void;\n /**\n * Optional buffered-byte sample. The dispatcher reads this on every\n * emit to detect sustained backpressure and close the connection\n * with the Graphorin `flow.throttled` code (4006) before the OS-\n * level send buffer collapses. Consumers backed by `@hono/node-ws`\n * can return the underlying `ws.bufferedAmount`. When the field is\n * not implemented, the dispatcher falls back to the per-connection\n * outstanding-event counter.\n */\n bufferedAmount?(): number;\n}\n\n/**\n * Snapshot of an active subscription. Surfaced via\n * {@link WsDispatcher.snapshotSubscription}.\n *\n * @stable\n */\nexport interface WsSubscriptionSnapshot {\n readonly subscriptionId: string;\n readonly subject: string;\n readonly subjectKind: ParsedSubject['kind'];\n readonly subscriberId: string;\n readonly tokenId: string;\n readonly createdAt: number;\n readonly lastEventId: string | undefined;\n}\n\n/**\n * Result of {@link WsDispatcher.subscribe}.\n *\n * @stable\n */\nexport type SubscribeResult =\n | {\n readonly ok: true;\n readonly subscription: WsSubscriptionSnapshot;\n readonly replayedCount: number;\n readonly snapshotEventId: string | undefined;\n }\n | {\n readonly ok: false;\n readonly reason:\n | 'subject-malformed'\n | 'subject-unknown'\n | 'subject-wildcard'\n | 'scope-denied';\n };\n\n/**\n * Public surface of the dispatcher.\n *\n * @stable\n */\nexport interface WsDispatcher {\n readonly sanitizer: DeliveryCommentarySanitizer;\n readonly replayBuffer: ReplayBuffer;\n /**\n * Register a subscriber (one per WebSocket connection).\n * `unregister` is called on close.\n */\n registerSubscriber(handle: WsSubscriberHandle): { unregister(): void };\n /**\n * Open a new subscription for an active subscriber. Returns either\n * the subscription snapshot + replayed-event count or a typed\n * failure reason the caller maps to the appropriate close code /\n * RPC error.\n */\n subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult;\n unsubscribe(subscriptionId: string): boolean;\n /**\n * Push an event into the dispatcher. Goes through the sanitizer,\n * validates against the protocol schema, persists into the replay\n * buffer, and fans out to every matching active subscription.\n */\n emit(subject: string, frame: BareEventFrame): void;\n /** Push a lifecycle frame to a single subscription. */\n emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void;\n /** List active subscriptions for a given subscriber (diagnostics). */\n listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot>;\n snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined;\n size(): { readonly subscribers: number; readonly subscriptions: number };\n /** Clear in-memory state (used during graceful shutdown). */\n shutdown(): void;\n}\n\n/**\n * Frame argument accepted by {@link WsDispatcher.emit}. The\n * dispatcher fills in `subscriptionId`, `subject`, and `eventId`.\n *\n * @stable\n */\nexport interface BareEventFrame {\n readonly type: string;\n readonly payload: unknown;\n readonly eventId?: string;\n}\n\ninterface SubscriberRecord {\n readonly handle: WsSubscriberHandle;\n readonly subscriptions: Set<string>;\n outstandingEvents: number;\n closed: boolean;\n}\n\ninterface SubscriptionRecord {\n readonly subscriberId: string;\n /** IP-18: the authenticating principal's token id (not the connection id). */\n readonly tokenId: string;\n readonly subscriptionId: string;\n readonly subject: string;\n readonly parsed: ParsedSubject;\n readonly createdAt: number;\n lastEventId: string | undefined;\n}\n\n/**\n * Build a fresh dispatcher.\n *\n * @stable\n */\nexport function createWsDispatcher(options: WsDispatcherOptions = {}): WsDispatcher {\n const sanitizer = createDeliveryCommentarySanitizer(options.commentary);\n const replayBuffer = createReplayBuffer(options.replayBuffer ?? {});\n const now = options.now ?? Date.now;\n const onWarn = options.onWarn;\n const perConnectionQueueLimit = options.perConnectionQueueLimit ?? 1_000;\n const subscribers = new Map<string, SubscriberRecord>();\n const subscriptions = new Map<string, SubscriptionRecord>();\n const subjectIndex = new Map<string, Set<string>>();\n let eventCounter = 0;\n\n function indexSubject(subject: string, subscriptionId: string): void {\n let set = subjectIndex.get(subject);\n if (set === undefined) {\n set = new Set();\n subjectIndex.set(subject, set);\n }\n set.add(subscriptionId);\n }\n\n function deindexSubject(subject: string, subscriptionId: string): void {\n const set = subjectIndex.get(subject);\n if (set === undefined) return;\n set.delete(subscriptionId);\n if (set.size === 0) subjectIndex.delete(subject);\n }\n\n function nextEventId(): string {\n eventCounter += 1;\n return `evt-${now().toString(36)}-${eventCounter.toString(36)}`;\n }\n\n function dispatchToSubscriber(record: SubscriptionRecord, frame: ServerMessage): void {\n const subscriber = subscribers.get(record.subscriberId);\n if (subscriber === undefined || subscriber.closed) return;\n // Per-connection backpressure: track the outstanding-event counter\n // and consult the optional `bufferedAmount()` sample. The\n // dispatcher closes the connection with the Graphorin\n // `flow.throttled` code (4006) when either signal exceeds the\n // configured limit so the client can reconnect against the\n // replay buffer instead of accumulating an unbounded send queue.\n const buffered =\n typeof subscriber.handle.bufferedAmount === 'function'\n ? subscriber.handle.bufferedAmount()\n : 0;\n if (\n subscriber.outstandingEvents >= perConnectionQueueLimit ||\n buffered >= perConnectionQueueLimit * 1024\n ) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n return;\n }\n subscriber.outstandingEvents += 1;\n try {\n subscriber.handle.send(frame);\n } catch (err) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n void err;\n } finally {\n subscriber.outstandingEvents = Math.max(0, subscriber.outstandingEvents - 1);\n }\n }\n\n function shutdownSubscriber(subscriber: SubscriberRecord, code: number, reason: string): void {\n if (subscriber.closed) return;\n subscriber.closed = true;\n try {\n subscriber.handle.close(code, reason);\n } catch {\n // ignore - we are tearing the subscriber down.\n }\n for (const id of subscriber.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscriber.subscriptions.clear();\n subscribers.delete(subscriber.handle.id);\n }\n\n function validateAndDispatch(record: SubscriptionRecord, frame: ServerMessage): void {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) {\n onWarn?.({\n kind: 'invalid-frame',\n subscriptionId: record.subscriptionId,\n issues: validated.error.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`),\n });\n return;\n }\n dispatchToSubscriber(record, validated.data);\n }\n\n function registerSubscriber(handle: WsSubscriberHandle): { unregister(): void } {\n subscribers.set(handle.id, {\n handle,\n subscriptions: new Set(),\n outstandingEvents: 0,\n closed: false,\n });\n return {\n unregister: () => {\n const record = subscribers.get(handle.id);\n if (record === undefined) return;\n for (const id of record.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscribers.delete(handle.id);\n },\n };\n }\n\n function subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult {\n const subscriber = subscribers.get(input.subscriberId);\n if (subscriber === undefined) {\n return { ok: false, reason: 'subject-unknown' };\n }\n const parseResult = tryParseSubject(input.subject);\n if (!parseResult.ok) {\n const reason = parseResult.reason;\n onWarn?.({ kind: 'subject-rejected', subject: input.subject, reason });\n if (reason === 'wildcard-not-supported') return { ok: false, reason: 'subject-wildcard' };\n if (reason === 'unknown-subject') return { ok: false, reason: 'subject-unknown' };\n return { ok: false, reason: 'subject-malformed' };\n }\n if (!isSubjectAllowed(subscriber.handle.grantedScopes, parseResult.subject)) {\n return { ok: false, reason: 'scope-denied' };\n }\n const record: SubscriptionRecord = {\n subscriberId: input.subscriberId,\n tokenId: subscriber.handle.tokenId,\n subscriptionId: input.subscriptionId,\n subject: input.subject,\n parsed: parseResult.subject,\n createdAt: now(),\n lastEventId: undefined,\n };\n subscriptions.set(input.subscriptionId, record);\n subscriber.subscriptions.add(input.subscriptionId);\n indexSubject(input.subject, input.subscriptionId);\n const replay = replayBuffer.replay(input.subject, input.sinceEventId);\n let replayed = 0;\n let snapshotEventId: string | undefined = replay.nextEventIdHint;\n for (const event of replay.events) {\n const frame: ServerEventFrame = {\n ...event,\n subscriptionId: input.subscriptionId,\n };\n validateAndDispatch(record, frame);\n record.lastEventId = frame.eventId;\n replayed += 1;\n }\n if (replay.droppedCount > 0) {\n validateAndDispatch(record, {\n v: '1',\n kind: 'replay-marker',\n subscriptionId: input.subscriptionId,\n eventId: snapshotEventId ?? `evt-replay-${now().toString(36)}`,\n droppedCount: replay.droppedCount,\n note: `Replay buffer dropped ${replay.droppedCount} events before resume.`,\n });\n snapshotEventId = snapshotEventId ?? `evt-replay-${now().toString(36)}`;\n }\n return {\n ok: true,\n subscription: snapshotForRecord(record),\n replayedCount: replayed,\n snapshotEventId,\n };\n }\n\n function unsubscribe(subscriptionId: string): boolean {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return false;\n const subscriber = subscribers.get(record.subscriberId);\n subscriber?.subscriptions.delete(subscriptionId);\n deindexSubject(record.subject, subscriptionId);\n subscriptions.delete(subscriptionId);\n return true;\n }\n\n function emit(subject: string, bare: BareEventFrame): void {\n const eventId = bare.eventId ?? nextEventId();\n // Buffer first so the replay layer captures every event, even\n // when there is no live subscriber. The buffered frame is\n // sanitizer-neutral (the per-subscription dispatch sanitizes\n // again on the wire so the second pass is idempotent - by\n // construction matching wraps are not re-wrapped).\n const bufferedFrame: ServerEventFrame = {\n v: '1',\n kind: 'event',\n eventId,\n subscriptionId: '__pending__',\n subject,\n type: bare.type,\n payload: bare.payload,\n };\n replayBuffer.push(subject, bufferedFrame);\n const subjectSubscribers = subjectIndex.get(subject);\n if (subjectSubscribers === undefined || subjectSubscribers.size === 0) return;\n for (const subscriptionId of subjectSubscribers) {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) continue;\n const sanitized = sanitizer.sanitize({ ...bufferedFrame, subscriptionId }, 'ws');\n validateAndDispatch(record, sanitized);\n record.lastEventId = sanitized.eventId;\n if (sanitized.eventId === eventId) continue;\n }\n void isEventFrame; // keep import warm\n }\n\n function emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return;\n const frame: ServerLifecycleFrame = {\n v: '1',\n kind: 'lifecycle',\n subscriptionId,\n status,\n ...(reason !== undefined ? { reason } : {}),\n };\n validateAndDispatch(record, frame);\n }\n\n function listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot> {\n const subscriber = subscribers.get(subscriberId);\n if (subscriber === undefined) return Object.freeze([]);\n return Object.freeze(\n [...subscriber.subscriptions]\n .map((id) => subscriptions.get(id))\n .filter((entry): entry is SubscriptionRecord => entry !== undefined)\n .map(snapshotForRecord),\n );\n }\n\n function snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return undefined;\n return snapshotForRecord(record);\n }\n\n function size(): { readonly subscribers: number; readonly subscriptions: number } {\n return Object.freeze({\n subscribers: subscribers.size,\n subscriptions: subscriptions.size,\n });\n }\n\n function shutdown(): void {\n for (const subscription of subscriptions.values()) {\n const subscriber = subscribers.get(subscription.subscriberId);\n try {\n subscriber?.handle.send({\n v: '1',\n kind: 'lifecycle',\n subscriptionId: subscription.subscriptionId,\n status: 'aborted',\n reason: 'server-shutdown',\n });\n } catch {\n // swallow - we are tearing down.\n }\n }\n subscriptions.clear();\n subjectIndex.clear();\n subscribers.clear();\n }\n\n return Object.freeze({\n sanitizer,\n replayBuffer,\n registerSubscriber,\n subscribe,\n unsubscribe,\n emit,\n emitLifecycle,\n listForSubscriber,\n snapshotSubscription,\n size,\n shutdown,\n });\n}\n\nfunction snapshotForRecord(record: SubscriptionRecord): WsSubscriptionSnapshot {\n return Object.freeze({\n subscriptionId: record.subscriptionId,\n subject: record.subject,\n subjectKind: record.parsed.kind,\n subscriberId: record.subscriberId,\n tokenId: record.tokenId,\n createdAt: record.createdAt,\n lastEventId: record.lastEventId,\n });\n}\n"],"mappings":"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"}
@@ -60,7 +60,7 @@ declare function tryParseSubject(raw: string): ParseSubjectResult;
60
60
  */
61
61
  declare function requiredScopeFor(subject: ParsedSubject): ParsedScope;
62
62
  /**
63
- * Compatibility shim re-exports `scopeMatches` so consumers don't
63
+ * Compatibility shim - re-exports `scopeMatches` so consumers don't
64
64
  * have to learn the security package's surface.
65
65
  *
66
66
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"subjects.d.ts","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;KAwBY,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA,kBAAA;;oBAC+B;;;;;;;;;;iBAiB3B,eAAA,eAA8B;;;;;;;;;iBAqD9B,gBAAA,UAA0B,gBAAgB;;;;;;;iBAyB1C,gBAAA,UACL,cAAc,uBACd"}
1
+ {"version":3,"file":"subjects.d.ts","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;KAwBY,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA,kBAAA;;oBAC+B;;;;;;;;;;iBAiB3B,eAAA,eAA8B;;;;;;;;;iBAqD9B,gBAAA,UAA0B,gBAAgB;;;;;;;iBA+B1C,gBAAA,UACL,cAAc,uBACd"}
@@ -86,8 +86,8 @@ function tryParseSubject(raw) {
86
86
  */
87
87
  function requiredScopeFor(subject) {
88
88
  switch (subject.kind) {
89
- case "session-events": return parseScope(`agents:invoke:${subject.sessionId}`);
90
- case "session-run-events": return parseScope(`agents:invoke:${subject.sessionId}`);
89
+ case "session-events": return parseScope(`sessions:read:${subject.sessionId}`);
90
+ case "session-run-events": return parseScope(`sessions:read:${subject.sessionId}`);
91
91
  case "agent-run-events": return parseScope(`agents:read:${subject.agentId}`);
92
92
  case "workflow-events": return parseScope(`workflows:read:${subject.workflowId}`);
93
93
  case "workflow-run-events": return parseScope(`workflows:read:${subject.workflowId}`);
@@ -96,7 +96,7 @@ function requiredScopeFor(subject) {
96
96
  }
97
97
  }
98
98
  /**
99
- * Compatibility shim re-exports `scopeMatches` so consumers don't
99
+ * Compatibility shim - re-exports `scopeMatches` so consumers don't
100
100
  * have to learn the security package's surface.
101
101
  *
102
102
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"subjects.js","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":["/**\n * Strict subject grammar enforced by the WS dispatcher. Subjects are\n * the multiplexing primitive over a single WS connection: every\n * subscription is keyed by a subject, every event carries the\n * subject it was emitted into, and the per-subject scope check\n * happens at subscription time.\n *\n * Wildcards are deferred to v0.2+ the dispatcher rejects any\n * subject containing `'*'` or `'#'` so the ACL surface stays\n * tractable.\n *\n * @packageDocumentation\n */\n\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\n\n/**\n * Discriminated union of every recognised subject form. Surfaced on\n * audit log entries + diagnostics; the wire still carries the raw\n * string.\n *\n * @stable\n */\nexport type ParsedSubject =\n | { readonly kind: 'session-events'; readonly sessionId: string }\n | {\n readonly kind: 'session-run-events';\n readonly sessionId: string;\n readonly runId: string;\n }\n | {\n readonly kind: 'agent-run-events';\n readonly agentId: string;\n readonly runId: string;\n }\n | { readonly kind: 'workflow-events'; readonly workflowId: string }\n | {\n readonly kind: 'workflow-run-events';\n readonly workflowId: string;\n readonly runId: string;\n }\n | { readonly kind: 'memory-conflicts' }\n | { readonly kind: 'audit-events' };\n\n/**\n * Result of {@link tryParseSubject}.\n *\n * @stable\n */\nexport type ParseSubjectResult =\n | { readonly ok: true; readonly subject: ParsedSubject }\n | {\n readonly ok: false;\n readonly reason: 'wildcard-not-supported' | 'unknown-subject' | 'malformed';\n };\n\nconst SESSION_EVENTS = /^session:([A-Za-z0-9_-]+)\\/events$/;\nconst SESSION_RUN_EVENTS = /^session:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst AGENT_RUN_EVENTS = /^agent:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_RUN_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\n\n/**\n * Parse a subject string into the {@link ParsedSubject} discriminator.\n *\n * @stable\n */\nexport function tryParseSubject(raw: string): ParseSubjectResult {\n if (typeof raw !== 'string' || raw.length === 0) {\n return { ok: false, reason: 'malformed' };\n }\n if (raw.includes('*') || raw.includes('#')) {\n return { ok: false, reason: 'wildcard-not-supported' };\n }\n if (raw === 'memory/conflicts') {\n return { ok: true, subject: { kind: 'memory-conflicts' } };\n }\n if (raw === 'audit/events') {\n return { ok: true, subject: { kind: 'audit-events' } };\n }\n let m = raw.match(SESSION_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'session-run-events', sessionId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(AGENT_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'agent-run-events', agentId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(SESSION_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'session-events', sessionId: m[1] ?? '' } };\n }\n m = raw.match(WORKFLOW_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'workflow-run-events', workflowId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(WORKFLOW_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'workflow-events', workflowId: m[1] ?? '' } };\n }\n return { ok: false, reason: 'unknown-subject' };\n}\n\n/**\n * Required scope literal for every subject kind, expressed as a\n * `ParsedScope`. The matcher `scopeMatches(granted, required)` uses\n * the standard wildcard rules from `@graphorin/security/auth`\n * (e.g. `agents:*` matches `agents:invoke:foo`).\n *\n * @stable\n */\nexport function requiredScopeFor(subject: ParsedSubject): ParsedScope {\n switch (subject.kind) {\n case 'session-events':\n return parseScope(`agents:invoke:${subject.sessionId}`);\n case 'session-run-events':\n return parseScope(`agents:invoke:${subject.sessionId}`);\n case 'agent-run-events':\n return parseScope(`agents:read:${subject.agentId}`);\n case 'workflow-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'workflow-run-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'memory-conflicts':\n return parseScope('memory:read');\n case 'audit-events':\n return parseScope('audit:read');\n }\n}\n\n/**\n * Compatibility shim re-exports `scopeMatches` so consumers don't\n * have to learn the security package's surface.\n *\n * @stable\n */\nexport function isSubjectAllowed(\n granted: ReadonlyArray<ParsedScope>,\n subject: ParsedSubject,\n): boolean {\n const required = requiredScopeFor(subject);\n for (const scope of granted) {\n if (scopeMatches(scope, required)) return true;\n }\n return false;\n}\n"],"mappings":";;;AAyDA,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;;;;;;AAO5B,SAAgB,gBAAgB,KAAiC;AAC/D,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAa;AAE3C,KAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,CACxC,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA0B;AAExD,KAAI,QAAQ,mBACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,oBAAoB;EAAE;AAE5D,KAAI,QAAQ,eACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,gBAAgB;EAAE;CAExD,IAAI,IAAI,IAAI,MAAM,mBAAmB;AACrC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAsB,WAAW,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAClF;AAEH,KAAI,IAAI,MAAM,iBAAiB;AAC/B,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAoB,SAAS,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAC9E;AAEH,KAAI,IAAI,MAAM,eAAe;AAC7B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAkB,WAAW,EAAE,MAAM;GAAI;EAAE;AAEjF,KAAI,IAAI,MAAM,oBAAoB;AAClC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAuB,YAAY,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EACpF;AAEH,KAAI,IAAI,MAAM,gBAAgB;AAC9B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAmB,YAAY,EAAE,MAAM;GAAI;EAAE;AAEnF,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAmB;;;;;;;;;;AAWjD,SAAgB,iBAAiB,SAAqC;AACpE,SAAQ,QAAQ,MAAhB;EACE,KAAK,iBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,qBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,mBACH,QAAO,WAAW,eAAe,QAAQ,UAAU;EACrD,KAAK,kBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,sBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,mBACH,QAAO,WAAW,cAAc;EAClC,KAAK,eACH,QAAO,WAAW,aAAa;;;;;;;;;AAUrC,SAAgB,iBACd,SACA,SACS;CACT,MAAM,WAAW,iBAAiB,QAAQ;AAC1C,MAAK,MAAM,SAAS,QAClB,KAAI,aAAa,OAAO,SAAS,CAAE,QAAO;AAE5C,QAAO"}
1
+ {"version":3,"file":"subjects.js","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":["/**\n * Strict subject grammar enforced by the WS dispatcher. Subjects are\n * the multiplexing primitive over a single WS connection: every\n * subscription is keyed by a subject, every event carries the\n * subject it was emitted into, and the per-subject scope check\n * happens at subscription time.\n *\n * Wildcards are deferred to v0.2+ - the dispatcher rejects any\n * subject containing `'*'` or `'#'` so the ACL surface stays\n * tractable.\n *\n * @packageDocumentation\n */\n\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\n\n/**\n * Discriminated union of every recognised subject form. Surfaced on\n * audit log entries + diagnostics; the wire still carries the raw\n * string.\n *\n * @stable\n */\nexport type ParsedSubject =\n | { readonly kind: 'session-events'; readonly sessionId: string }\n | {\n readonly kind: 'session-run-events';\n readonly sessionId: string;\n readonly runId: string;\n }\n | {\n readonly kind: 'agent-run-events';\n readonly agentId: string;\n readonly runId: string;\n }\n | { readonly kind: 'workflow-events'; readonly workflowId: string }\n | {\n readonly kind: 'workflow-run-events';\n readonly workflowId: string;\n readonly runId: string;\n }\n | { readonly kind: 'memory-conflicts' }\n | { readonly kind: 'audit-events' };\n\n/**\n * Result of {@link tryParseSubject}.\n *\n * @stable\n */\nexport type ParseSubjectResult =\n | { readonly ok: true; readonly subject: ParsedSubject }\n | {\n readonly ok: false;\n readonly reason: 'wildcard-not-supported' | 'unknown-subject' | 'malformed';\n };\n\nconst SESSION_EVENTS = /^session:([A-Za-z0-9_-]+)\\/events$/;\nconst SESSION_RUN_EVENTS = /^session:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst AGENT_RUN_EVENTS = /^agent:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_RUN_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\n\n/**\n * Parse a subject string into the {@link ParsedSubject} discriminator.\n *\n * @stable\n */\nexport function tryParseSubject(raw: string): ParseSubjectResult {\n if (typeof raw !== 'string' || raw.length === 0) {\n return { ok: false, reason: 'malformed' };\n }\n if (raw.includes('*') || raw.includes('#')) {\n return { ok: false, reason: 'wildcard-not-supported' };\n }\n if (raw === 'memory/conflicts') {\n return { ok: true, subject: { kind: 'memory-conflicts' } };\n }\n if (raw === 'audit/events') {\n return { ok: true, subject: { kind: 'audit-events' } };\n }\n let m = raw.match(SESSION_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'session-run-events', sessionId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(AGENT_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'agent-run-events', agentId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(SESSION_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'session-events', sessionId: m[1] ?? '' } };\n }\n m = raw.match(WORKFLOW_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'workflow-run-events', workflowId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(WORKFLOW_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'workflow-events', workflowId: m[1] ?? '' } };\n }\n return { ok: false, reason: 'unknown-subject' };\n}\n\n/**\n * Required scope literal for every subject kind, expressed as a\n * `ParsedScope`. The matcher `scopeMatches(granted, required)` uses\n * the standard wildcard rules from `@graphorin/security/auth`\n * (e.g. `agents:*` matches `agents:invoke:foo`).\n *\n * @stable\n */\nexport function requiredScopeFor(subject: ParsedSubject): ParsedScope {\n switch (subject.kind) {\n // periphery-10: session streams are READ-ONLY, so they gate on\n // `sessions:read:<sessionId>` - consistent with the SSE route's\n // `sessions:read` requirement (which the old `agents:invoke:<x>`\n // requirement silently stacked on top of), and the resource slot\n // is a sessionId under the sessions family instead of overloading\n // `agents:invoke` (whose slot is an agentId everywhere else).\n case 'session-events':\n return parseScope(`sessions:read:${subject.sessionId}`);\n case 'session-run-events':\n return parseScope(`sessions:read:${subject.sessionId}`);\n case 'agent-run-events':\n return parseScope(`agents:read:${subject.agentId}`);\n case 'workflow-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'workflow-run-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'memory-conflicts':\n return parseScope('memory:read');\n case 'audit-events':\n return parseScope('audit:read');\n }\n}\n\n/**\n * Compatibility shim - re-exports `scopeMatches` so consumers don't\n * have to learn the security package's surface.\n *\n * @stable\n */\nexport function isSubjectAllowed(\n granted: ReadonlyArray<ParsedScope>,\n subject: ParsedSubject,\n): boolean {\n const required = requiredScopeFor(subject);\n for (const scope of granted) {\n if (scopeMatches(scope, required)) return true;\n }\n return false;\n}\n"],"mappings":";;;AAyDA,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;;;;;;AAO5B,SAAgB,gBAAgB,KAAiC;AAC/D,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAa;AAE3C,KAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,CACxC,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA0B;AAExD,KAAI,QAAQ,mBACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,oBAAoB;EAAE;AAE5D,KAAI,QAAQ,eACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,gBAAgB;EAAE;CAExD,IAAI,IAAI,IAAI,MAAM,mBAAmB;AACrC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAsB,WAAW,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAClF;AAEH,KAAI,IAAI,MAAM,iBAAiB;AAC/B,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAoB,SAAS,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAC9E;AAEH,KAAI,IAAI,MAAM,eAAe;AAC7B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAkB,WAAW,EAAE,MAAM;GAAI;EAAE;AAEjF,KAAI,IAAI,MAAM,oBAAoB;AAClC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAuB,YAAY,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EACpF;AAEH,KAAI,IAAI,MAAM,gBAAgB;AAC9B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAmB,YAAY,EAAE,MAAM;GAAI;EAAE;AAEnF,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAmB;;;;;;;;;;AAWjD,SAAgB,iBAAiB,SAAqC;AACpE,SAAQ,QAAQ,MAAhB;EAOE,KAAK,iBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,qBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,mBACH,QAAO,WAAW,eAAe,QAAQ,UAAU;EACrD,KAAK,kBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,sBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,mBACH,QAAO,WAAW,cAAc;EAClC,KAAK,eACH,QAAO,WAAW,aAAa;;;;;;;;;AAUrC,SAAgB,iBACd,SACA,SACS;CACT,MAAM,WAAW,iBAAiB,QAAQ;AAC1C,MAAK,MAAM,SAAS,QAClB,KAAI,aAAa,OAAO,SAAS,CAAE,QAAO;AAE5C,QAAO"}
@@ -63,7 +63,7 @@ interface WsTicketStore {
63
63
  /**
64
64
  * Build the default in-memory ticket store. Production deployments
65
65
  * use exactly one store per process (multiple processes would each
66
- * issue their own tickets there is no shared state because the
66
+ * issue their own tickets - there is no shared state because the
67
67
  * single-user-per-process default applies).
68
68
  *
69
69
  * @stable
package/dist/ws/ticket.js CHANGED
@@ -22,7 +22,7 @@ import { randomBytes } from "node:crypto";
22
22
  /**
23
23
  * Build the default in-memory ticket store. Production deployments
24
24
  * use exactly one store per process (multiple processes would each
25
- * issue their own tickets there is no shared state because the
25
+ * issue their own tickets - there is no shared state because the
26
26
  * single-user-per-process default applies).
27
27
  *
28
28
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"ticket.js","names":["ticket: WsTicket"],"sources":["../../src/ws/ticket.ts"],"sourcesContent":["/**\n * In-memory single-use ticket store for browser WebSocket clients.\n * The browser WebSocket API does not allow custom headers, so the\n * server issues a short-lived ticket via `POST /v1/session/ws-ticket`\n * (HTTP-authenticated); the client then attaches the value as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`) on the\n * upgrade request.\n *\n * Tickets:\n * - default 5-minute TTL,\n * - single-use (the first valid `consume()` call marks the ticket\n * consumed; every subsequent attempt rejects),\n * - in-memory only (server restart invalidates everything; CLI /\n * SDK clients use HTTP bearer instead so the lost ticket window\n * does not affect them).\n *\n * @packageDocumentation\n */\n\nimport { randomBytes } from 'node:crypto';\nimport type { ParsedScope } from '@graphorin/security/auth';\n\n/**\n * Stable shape returned by {@link WsTicketStore.issue}.\n *\n * @stable\n */\nexport interface WsTicket {\n readonly value: string;\n readonly expiresAt: number;\n readonly issuedAt: number;\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n}\n\n/**\n * Stable result of {@link WsTicketStore.consume}.\n *\n * @stable\n */\nexport type WsTicketConsumeResult =\n | { readonly ok: true; readonly ticket: WsTicket }\n | { readonly ok: false; readonly reason: 'unknown' | 'consumed' | 'expired' };\n\n/**\n * Options accepted by {@link createWsTicketStore}.\n *\n * @stable\n */\nexport interface WsTicketStoreOptions {\n /** Default `300_000` (5 min). */\n readonly ttlMs?: number;\n /** Cap on the number of unconsumed tickets retained. Default `1_000`. */\n readonly maxOutstanding?: number;\n readonly now?: () => number;\n /**\n * Random-bytes generator. Tests pass a deterministic source so\n * ticket values are reproducible.\n */\n readonly randomBytes?: (length: number) => Uint8Array;\n}\n\n/**\n * Pluggable in-memory ticket store used by the WS upgrade handler +\n * the `POST /v1/session/ws-ticket` route.\n *\n * @stable\n */\nexport interface WsTicketStore {\n readonly ttlMs: number;\n issue(input: { readonly tokenId: string; readonly scopes: ReadonlyArray<ParsedScope> }): WsTicket;\n consume(value: string): WsTicketConsumeResult;\n /** Drop expired entries; called on every `consume()`. */\n prune(): number;\n size(): number;\n}\n\ninterface InternalEntry {\n readonly ticket: WsTicket;\n consumed: boolean;\n}\n\n/**\n * Build the default in-memory ticket store. Production deployments\n * use exactly one store per process (multiple processes would each\n * issue their own tickets there is no shared state because the\n * single-user-per-process default applies).\n *\n * @stable\n */\nexport function createWsTicketStore(options: WsTicketStoreOptions = {}): WsTicketStore {\n const ttlMs = options.ttlMs ?? 5 * 60_000;\n const maxOutstanding = options.maxOutstanding ?? 1_000;\n const now = options.now ?? Date.now;\n const random = options.randomBytes ?? ((n: number) => randomBytes(n));\n const entries = new Map<string, InternalEntry>();\n\n function evictOldest(): void {\n const oldest = entries.keys().next().value;\n if (oldest !== undefined) entries.delete(oldest);\n }\n\n function prune(): number {\n const cutoff = now();\n let removed = 0;\n for (const [value, entry] of entries) {\n if (entry.ticket.expiresAt <= cutoff) {\n entries.delete(value);\n removed += 1;\n }\n }\n return removed;\n }\n\n function issue(input: {\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n }): WsTicket {\n prune();\n while (entries.size >= maxOutstanding) {\n evictOldest();\n }\n const issuedAt = now();\n const value = encodeTicket(random(24));\n const ticket: WsTicket = {\n value,\n issuedAt,\n expiresAt: issuedAt + ttlMs,\n tokenId: input.tokenId,\n scopes: Object.freeze(input.scopes.slice()),\n };\n entries.set(value, { ticket, consumed: false });\n return ticket;\n }\n\n function consume(value: string): WsTicketConsumeResult {\n const entry = entries.get(value);\n if (entry === undefined) {\n prune();\n return { ok: false, reason: 'unknown' };\n }\n if (entry.ticket.expiresAt <= now()) {\n entries.delete(value);\n prune();\n return { ok: false, reason: 'expired' };\n }\n if (entry.consumed) {\n prune();\n return { ok: false, reason: 'consumed' };\n }\n entry.consumed = true;\n prune();\n return { ok: true, ticket: entry.ticket };\n }\n\n return Object.freeze({\n ttlMs,\n issue,\n consume,\n prune,\n size: () => entries.size,\n });\n}\n\nfunction encodeTicket(bytes: Uint8Array): string {\n // URL-safe base64 Buffer.from is available in every supported\n // Node runtime; the implementation never touches the value once\n // encoded so the choice is purely cosmetic.\n return Buffer.from(bytes)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,oBAAoB,UAAgC,EAAE,EAAiB;CACrF,MAAM,QAAQ,QAAQ,SAAS,IAAI;CACnC,MAAM,iBAAiB,QAAQ,kBAAkB;CACjD,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,SAAS,QAAQ,iBAAiB,MAAc,YAAY,EAAE;CACpE,MAAM,0BAAU,IAAI,KAA4B;CAEhD,SAAS,cAAoB;EAC3B,MAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,CAAC;AACrC,MAAI,WAAW,OAAW,SAAQ,OAAO,OAAO;;CAGlD,SAAS,QAAgB;EACvB,MAAM,SAAS,KAAK;EACpB,IAAI,UAAU;AACd,OAAK,MAAM,CAAC,OAAO,UAAU,QAC3B,KAAI,MAAM,OAAO,aAAa,QAAQ;AACpC,WAAQ,OAAO,MAAM;AACrB,cAAW;;AAGf,SAAO;;CAGT,SAAS,MAAM,OAGF;AACX,SAAO;AACP,SAAO,QAAQ,QAAQ,eACrB,cAAa;EAEf,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,aAAa,OAAO,GAAG,CAAC;EACtC,MAAMA,SAAmB;GACvB;GACA;GACA,WAAW,WAAW;GACtB,SAAS,MAAM;GACf,QAAQ,OAAO,OAAO,MAAM,OAAO,OAAO,CAAC;GAC5C;AACD,UAAQ,IAAI,OAAO;GAAE;GAAQ,UAAU;GAAO,CAAC;AAC/C,SAAO;;CAGT,SAAS,QAAQ,OAAsC;EACrD,MAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,MAAI,UAAU,QAAW;AACvB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,OAAO,aAAa,KAAK,EAAE;AACnC,WAAQ,OAAO,MAAM;AACrB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,UAAU;AAClB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAY;;AAE1C,QAAM,WAAW;AACjB,SAAO;AACP,SAAO;GAAE,IAAI;GAAM,QAAQ,MAAM;GAAQ;;AAG3C,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EACA;EACA,YAAY,QAAQ;EACrB,CAAC;;AAGJ,SAAS,aAAa,OAA2B;AAI/C,QAAO,OAAO,KAAK,MAAM,CACtB,SAAS,SAAS,CAClB,QAAQ,OAAO,IAAI,CACnB,QAAQ,OAAO,IAAI,CACnB,QAAQ,QAAQ,GAAG"}
1
+ {"version":3,"file":"ticket.js","names":["ticket: WsTicket"],"sources":["../../src/ws/ticket.ts"],"sourcesContent":["/**\n * In-memory single-use ticket store for browser WebSocket clients.\n * The browser WebSocket API does not allow custom headers, so the\n * server issues a short-lived ticket via `POST /v1/session/ws-ticket`\n * (HTTP-authenticated); the client then attaches the value as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`) on the\n * upgrade request.\n *\n * Tickets:\n * - default 5-minute TTL,\n * - single-use (the first valid `consume()` call marks the ticket\n * consumed; every subsequent attempt rejects),\n * - in-memory only (server restart invalidates everything; CLI /\n * SDK clients use HTTP bearer instead so the lost ticket window\n * does not affect them).\n *\n * @packageDocumentation\n */\n\nimport { randomBytes } from 'node:crypto';\nimport type { ParsedScope } from '@graphorin/security/auth';\n\n/**\n * Stable shape returned by {@link WsTicketStore.issue}.\n *\n * @stable\n */\nexport interface WsTicket {\n readonly value: string;\n readonly expiresAt: number;\n readonly issuedAt: number;\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n}\n\n/**\n * Stable result of {@link WsTicketStore.consume}.\n *\n * @stable\n */\nexport type WsTicketConsumeResult =\n | { readonly ok: true; readonly ticket: WsTicket }\n | { readonly ok: false; readonly reason: 'unknown' | 'consumed' | 'expired' };\n\n/**\n * Options accepted by {@link createWsTicketStore}.\n *\n * @stable\n */\nexport interface WsTicketStoreOptions {\n /** Default `300_000` (5 min). */\n readonly ttlMs?: number;\n /** Cap on the number of unconsumed tickets retained. Default `1_000`. */\n readonly maxOutstanding?: number;\n readonly now?: () => number;\n /**\n * Random-bytes generator. Tests pass a deterministic source so\n * ticket values are reproducible.\n */\n readonly randomBytes?: (length: number) => Uint8Array;\n}\n\n/**\n * Pluggable in-memory ticket store used by the WS upgrade handler +\n * the `POST /v1/session/ws-ticket` route.\n *\n * @stable\n */\nexport interface WsTicketStore {\n readonly ttlMs: number;\n issue(input: { readonly tokenId: string; readonly scopes: ReadonlyArray<ParsedScope> }): WsTicket;\n consume(value: string): WsTicketConsumeResult;\n /** Drop expired entries; called on every `consume()`. */\n prune(): number;\n size(): number;\n}\n\ninterface InternalEntry {\n readonly ticket: WsTicket;\n consumed: boolean;\n}\n\n/**\n * Build the default in-memory ticket store. Production deployments\n * use exactly one store per process (multiple processes would each\n * issue their own tickets - there is no shared state because the\n * single-user-per-process default applies).\n *\n * @stable\n */\nexport function createWsTicketStore(options: WsTicketStoreOptions = {}): WsTicketStore {\n const ttlMs = options.ttlMs ?? 5 * 60_000;\n const maxOutstanding = options.maxOutstanding ?? 1_000;\n const now = options.now ?? Date.now;\n const random = options.randomBytes ?? ((n: number) => randomBytes(n));\n const entries = new Map<string, InternalEntry>();\n\n function evictOldest(): void {\n const oldest = entries.keys().next().value;\n if (oldest !== undefined) entries.delete(oldest);\n }\n\n function prune(): number {\n const cutoff = now();\n let removed = 0;\n for (const [value, entry] of entries) {\n if (entry.ticket.expiresAt <= cutoff) {\n entries.delete(value);\n removed += 1;\n }\n }\n return removed;\n }\n\n function issue(input: {\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n }): WsTicket {\n prune();\n while (entries.size >= maxOutstanding) {\n evictOldest();\n }\n const issuedAt = now();\n const value = encodeTicket(random(24));\n const ticket: WsTicket = {\n value,\n issuedAt,\n expiresAt: issuedAt + ttlMs,\n tokenId: input.tokenId,\n scopes: Object.freeze(input.scopes.slice()),\n };\n entries.set(value, { ticket, consumed: false });\n return ticket;\n }\n\n function consume(value: string): WsTicketConsumeResult {\n const entry = entries.get(value);\n if (entry === undefined) {\n prune();\n return { ok: false, reason: 'unknown' };\n }\n if (entry.ticket.expiresAt <= now()) {\n entries.delete(value);\n prune();\n return { ok: false, reason: 'expired' };\n }\n if (entry.consumed) {\n prune();\n return { ok: false, reason: 'consumed' };\n }\n entry.consumed = true;\n prune();\n return { ok: true, ticket: entry.ticket };\n }\n\n return Object.freeze({\n ttlMs,\n issue,\n consume,\n prune,\n size: () => entries.size,\n });\n}\n\nfunction encodeTicket(bytes: Uint8Array): string {\n // URL-safe base64 - Buffer.from is available in every supported\n // Node runtime; the implementation never touches the value once\n // encoded so the choice is purely cosmetic.\n return Buffer.from(bytes)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,oBAAoB,UAAgC,EAAE,EAAiB;CACrF,MAAM,QAAQ,QAAQ,SAAS,IAAI;CACnC,MAAM,iBAAiB,QAAQ,kBAAkB;CACjD,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,SAAS,QAAQ,iBAAiB,MAAc,YAAY,EAAE;CACpE,MAAM,0BAAU,IAAI,KAA4B;CAEhD,SAAS,cAAoB;EAC3B,MAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,CAAC;AACrC,MAAI,WAAW,OAAW,SAAQ,OAAO,OAAO;;CAGlD,SAAS,QAAgB;EACvB,MAAM,SAAS,KAAK;EACpB,IAAI,UAAU;AACd,OAAK,MAAM,CAAC,OAAO,UAAU,QAC3B,KAAI,MAAM,OAAO,aAAa,QAAQ;AACpC,WAAQ,OAAO,MAAM;AACrB,cAAW;;AAGf,SAAO;;CAGT,SAAS,MAAM,OAGF;AACX,SAAO;AACP,SAAO,QAAQ,QAAQ,eACrB,cAAa;EAEf,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,aAAa,OAAO,GAAG,CAAC;EACtC,MAAMA,SAAmB;GACvB;GACA;GACA,WAAW,WAAW;GACtB,SAAS,MAAM;GACf,QAAQ,OAAO,OAAO,MAAM,OAAO,OAAO,CAAC;GAC5C;AACD,UAAQ,IAAI,OAAO;GAAE;GAAQ,UAAU;GAAO,CAAC;AAC/C,SAAO;;CAGT,SAAS,QAAQ,OAAsC;EACrD,MAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,MAAI,UAAU,QAAW;AACvB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,OAAO,aAAa,KAAK,EAAE;AACnC,WAAQ,OAAO,MAAM;AACrB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,UAAU;AAClB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAY;;AAE1C,QAAM,WAAW;AACjB,SAAO;AACP,SAAO;GAAE,IAAI;GAAM,QAAQ,MAAM;GAAQ;;AAG3C,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EACA;EACA,YAAY,QAAQ;EACrB,CAAC;;AAGJ,SAAS,aAAa,OAA2B;AAI/C,QAAO,OAAO,KAAK,MAAM,CACtB,SAAS,SAAS,CAClB,QAAQ,OAAO,IAAI,CACnB,QAAQ,OAAO,IAAI,CACnB,QAAQ,QAAQ,GAAG"}
@@ -12,14 +12,14 @@ import { parseScope, scopeMatches } from "@graphorin/security/auth";
12
12
  * ## Subprotocol + browser ticket flow
13
13
  *
14
14
  * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}
15
- * (`graphorin.protocol.v1`) the canonical wire contract lives in
15
+ * (`graphorin.protocol.v1`) - the canonical wire contract lives in
16
16
  * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:
17
17
  *
18
18
  * - **Bearer (non-browser):** the client sends `Authorization:
19
19
  * Bearer <token>` and a single `Sec-WebSocket-Protocol:
20
20
  * graphorin.protocol.v1` token.
21
21
  * - **Ticket (browser):** the `WebSocket` constructor cannot set
22
- * headers, so the browser client offers two subprotocol tokens
22
+ * headers, so the browser client offers two subprotocol tokens -
23
23
  * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes
24
24
  * back only the canonical name (in `app.ts`'s `handleProtocols`),
25
25
  * and {@link resolveUpgradeAuth} extracts the ticket via
@@ -100,7 +100,7 @@ async function createWsUpgradeEvents(c, options) {
100
100
  sendRpcSuccess(ws, message.id, {
101
101
  serverInfo: {
102
102
  name: "graphorin-server",
103
- version: "0.5.0"
103
+ version: "0.6.0"
104
104
  },
105
105
  capabilities: {
106
106
  subscriptions: true,
@@ -251,7 +251,8 @@ async function resolveUpgradeAuth(c, options) {
251
251
  reason: "auth.required"
252
252
  };
253
253
  const token = header.slice(7).trim();
254
- const verified = await options.verifier.verify(token);
254
+ const ip = c.get("state")?.clientIp;
255
+ const verified = await options.verifier.verify(token, ip !== void 0 ? { ip } : {});
255
256
  if (!verified.ok) return {
256
257
  kind: "denied",
257
258
  reason: "auth.invalid"
@@ -269,7 +270,7 @@ async function resolveUpgradeAuth(c, options) {
269
270
  }
270
271
  /**
271
272
  * IP-8: the `agents:invoke` scope required to cancel a run over the WS
272
- * transport the same requirement the REST `POST /runs/:runId/abort` route
273
+ * transport - the same requirement the REST `POST /runs/:runId/abort` route
273
274
  * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).
274
275
  */
275
276
  const INVOKE_SCOPE = parseScope("agents:invoke");
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","frame: ServerMessage","INVOKE_SCOPE: ParsedScope","ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope>"],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":["/**\n * Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter\n * to the dispatcher: validates the subprotocol, runs auth (bearer\n * via `requireAuth` middleware OR ticket via the in-memory store),\n * and bridges the per-connection `WSContext` callbacks to the\n * dispatcher's RPC handler.\n *\n * ## Subprotocol + browser ticket flow\n *\n * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}\n * (`graphorin.protocol.v1`) — the canonical wire contract lives in\n * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:\n *\n * - **Bearer (non-browser):** the client sends `Authorization:\n * Bearer <token>` and a single `Sec-WebSocket-Protocol:\n * graphorin.protocol.v1` token.\n * - **Ticket (browser):** the `WebSocket` constructor cannot set\n * headers, so the browser client offers two subprotocol tokens —\n * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes\n * back only the canonical name (in `app.ts`'s `handleProtocols`),\n * and {@link resolveUpgradeAuth} extracts the ticket via\n * `parseTicketSubprotocol` and redeems it against the single-use\n * {@link WsTicketStore}. Tickets are short-lived and one-shot to\n * bound replay.\n *\n * @packageDocumentation\n */\n\nimport {\n ClientMessageSchema,\n closeCodeFor,\n isCancelledNotification,\n isInitializeRequest,\n isPingRequest,\n isRunCancelRequest,\n isSubscribeRequest,\n isUnsubscribeRequest,\n negotiateSubprotocol,\n parseTicketSubprotocol,\n RPC_ERROR_CODES,\n type ServerMessage,\n SUBPROTOCOL_NAME,\n} from '@graphorin/protocol';\nimport type { ParsedScope, TokenVerifier } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { Context } from 'hono';\nimport type { WSContext, WSEvents } from 'hono/ws';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\nimport type { WsDispatcher } from './dispatcher.js';\nimport type { WsTicket, WsTicketStore } from './ticket.js';\n\n/**\n * Public configuration accepted by {@link createWsUpgradeEvents}.\n *\n * @stable\n */\nexport interface WsUpgradeOptions {\n readonly dispatcher: WsDispatcher;\n readonly tickets: WsTicketStore;\n /**\n * Token verifier for bearer / ticket upgrades. Optional only in the\n * IP-13 no-auth loopback mode (`anonymous: true`), where there is no\n * verifier to construct and every upgrade is accepted.\n */\n readonly verifier?: TokenVerifier;\n /**\n * IP-13: authentication is disabled server-wide (`auth.kind='none'`).\n * Accept the upgrade unconditionally with a full (`admin:*`) scope grant\n * instead of silently refusing to mount the WS route. Trusted-loopback\n * / single-operator mode only.\n */\n readonly anonymous?: boolean;\n readonly runs?: RunStateTracker;\n readonly now?: () => number;\n /**\n * Subprotocol the server advertises. Defaults to\n * {@link SUBPROTOCOL_NAME}; tests can override to exercise the\n * mismatch path.\n */\n readonly serverSubprotocol?: string;\n readonly newSubscriptionId?: () => string;\n readonly newSubscriberId?: () => string;\n}\n\n/**\n * Build the `WSEvents` callback bag the Hono helper consumes. The\n * function takes the request `Context` so the upgrade can read the\n * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.\n *\n * Production wiring on `@hono/node-ws`:\n *\n * ```ts\n * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });\n * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));\n * injectWebSocket(serve(...));\n * ```\n *\n * @stable\n */\nexport async function createWsUpgradeEvents(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<WSEvents> {\n const negotiated = negotiateSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (negotiated === null) {\n return rejectImmediately(closeCodeFor('protocol.violation'), 'protocol.violation');\n }\n const auth = await resolveUpgradeAuth(c, options);\n if (auth.kind === 'denied') {\n return rejectImmediately(closeCodeFor(auth.reason), auth.reason);\n }\n\n const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();\n const newSubscriptionId =\n options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));\n\n let unregister: (() => void) | undefined;\n let initialized = false;\n\n return {\n onOpen: (_event, ws) => {\n const handle = {\n id: subscriberId,\n tokenId: auth.tokenId,\n grantedScopes: auth.grantedScopes,\n send: (frame: ServerMessage) => {\n ws.send(JSON.stringify(frame));\n },\n close: (code: number, reason: string) => {\n ws.close(code, reason);\n },\n // IP-9: surface the socket's real backlog so the dispatcher's\n // backpressure threshold measures something — the synchronous\n // outstanding-events counter never accumulates.\n bufferedAmount: () => {\n const raw = (ws as { raw?: { bufferedAmount?: number } }).raw;\n return raw?.bufferedAmount ?? 0;\n },\n };\n const reg = options.dispatcher.registerSubscriber(handle);\n unregister = reg.unregister;\n },\n onMessage: (event, ws) => {\n const raw = typeof event.data === 'string' ? event.data : '';\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n sendRpcError(ws, undefined, RPC_ERROR_CODES.PARSE_ERROR, 'Frame is not valid JSON.');\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const parsed = ClientMessageSchema.safeParse(payload);\n if (!parsed.success) {\n sendRpcError(\n ws,\n (payload as { id?: string | number } | null)?.id,\n RPC_ERROR_CODES.INVALID_REQUEST,\n 'Frame failed schema validation.',\n );\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const message = parsed.data;\n if (isCancelledNotification(message)) {\n // IP-8: only an initialized connection holding `agents:invoke` may\n // cancel a run. A notification carries no id, so an unauthorised one\n // is silently ignored (there is no error channel) rather than aborting\n // an arbitrary run.\n if (initialized && grantsInvokeScope(auth.grantedScopes)) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\n return;\n }\n if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {\n // IP-21: a frame before `initialize` is a protocol-sequencing error,\n // not an auth failure — the connection is already authenticated.\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.PROTOCOL_VIOLATION,\n 'Send `initialize` before any other RPC.',\n );\n return;\n }\n if (isInitializeRequest(message)) {\n initialized = true;\n sendRpcSuccess(ws, message.id, {\n serverInfo: {\n name: 'graphorin-server',\n version: '0.5.0',\n },\n capabilities: {\n subscriptions: true,\n replayBuffer: true,\n sseFallback: true,\n },\n });\n return;\n }\n if (isPingRequest(message)) {\n const nonce = message.params?.nonce;\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'pong',\n ...(nonce !== undefined ? { nonce } : {}),\n } satisfies ServerMessage),\n );\n sendRpcSuccess(ws, message.id, { ok: true });\n return;\n }\n if (isSubscribeRequest(message)) {\n const subscriptionId = newSubscriptionId();\n const result = options.dispatcher.subscribe({\n subscriberId,\n subject: message.params.subject,\n subscriptionId,\n ...(message.params.sinceEventId !== undefined\n ? { sinceEventId: message.params.sinceEventId }\n : {}),\n });\n if (!result.ok) {\n sendRpcError(\n ws,\n message.id,\n mapSubscribeErrorCode(result.reason),\n mapSubscribeErrorMessage(result.reason, message.params.subject),\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId,\n subject: message.params.subject,\n snapshotEventId: result.snapshotEventId,\n replayedCount: result.replayedCount,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'subscribed',\n subscriptionId,\n subject: message.params.subject,\n ...(result.snapshotEventId !== undefined\n ? { snapshotEventId: result.snapshotEventId }\n : {}),\n } satisfies ServerMessage),\n );\n return;\n }\n if (isUnsubscribeRequest(message)) {\n const removed = options.dispatcher.unsubscribe(message.params.subscriptionId);\n if (!removed) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,\n `Subscription '${message.params.subscriptionId}' not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId: message.params.subscriptionId,\n unsubscribed: true,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'unsubscribed',\n subscriptionId: message.params.subscriptionId,\n } satisfies ServerMessage),\n );\n return;\n }\n if (isRunCancelRequest(message)) {\n // IP-8: mirror the REST `POST /runs/:runId/abort` scope gate so a\n // bearer token cannot abort an arbitrary run without `agents:invoke`.\n if (!grantsInvokeScope(auth.grantedScopes)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n \"Token lacks required scope 'agents:invoke'.\",\n );\n return;\n }\n const runId = message.params.runId;\n const aborted = options.runs?.abort(runId, message.params.reason ?? 'rpc-cancel') === true;\n if (!aborted) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.RUN_NOT_FOUND,\n `Run '${runId}' is not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n runId,\n cancelled: true,\n partialStateAvailable: true,\n });\n return;\n }\n },\n onClose: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n onError: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n };\n}\n\nfunction sendRpcSuccess(ws: WSContext, id: string | number, result: unknown): void {\n const frame: ServerMessage = { v: '1', jsonrpc: '2.0', id, result };\n ws.send(JSON.stringify(frame));\n}\n\nfunction sendRpcError(\n ws: WSContext,\n id: string | number | undefined,\n code: number,\n message: string,\n): void {\n const frame: ServerMessage = {\n v: '1',\n jsonrpc: '2.0',\n id: id ?? 0,\n error: { code, message },\n };\n ws.send(JSON.stringify(frame));\n}\n\nfunction rejectImmediately(code: number, reason: string): WSEvents {\n return {\n onOpen: (_event, ws) => {\n ws.close(code, reason);\n },\n };\n}\n\ninterface ResolvedUpgradeAuthOk {\n readonly kind: 'ok';\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n}\n\ninterface ResolvedUpgradeAuthDenied {\n readonly kind: 'denied';\n readonly reason: 'auth.required' | 'auth.invalid' | 'auth.scope_denied';\n}\n\ntype ResolvedUpgradeAuth = ResolvedUpgradeAuthOk | ResolvedUpgradeAuthDenied;\n\nasync function resolveUpgradeAuth(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<ResolvedUpgradeAuth> {\n // Phase 14a's HTTP middleware may have already verified the\n // bearer token (`requireAuth` runs before the upgrade route on\n // the authenticated subtree); if so, reuse the result.\n const state = c.var.state;\n if (state?.auth?.kind === 'token') {\n return {\n kind: 'ok',\n tokenId: state.auth.token.tokenId,\n grantedScopes: state.auth.grantedScopes,\n };\n }\n // IP-13: no-auth loopback mode — accept the upgrade with a full scope grant\n // instead of silently failing to mount. Checked before ticket/bearer so the\n // verifier may legitimately be absent.\n if (options.anonymous === true) {\n return { kind: 'ok', tokenId: 'anonymous', grantedScopes: ANONYMOUS_WS_SCOPES };\n }\n const ticketValue = parseTicketSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (ticketValue !== undefined) {\n const consumed = options.tickets.consume(ticketValue);\n if (!consumed.ok) {\n return { kind: 'denied', reason: 'auth.invalid' };\n }\n return acceptTicket(consumed.ticket);\n }\n const header = c.req.header('authorization') ?? c.req.header('Authorization');\n if (header?.toLowerCase().startsWith('bearer ') === true) {\n if (options.verifier === undefined) return { kind: 'denied', reason: 'auth.required' };\n const token = header.slice(7).trim();\n const verified = await options.verifier.verify(token);\n if (!verified.ok) return { kind: 'denied', reason: 'auth.invalid' };\n return {\n kind: 'ok',\n tokenId: verified.token.tokenId,\n grantedScopes: verified.token.scopes,\n };\n }\n return { kind: 'denied', reason: 'auth.required' };\n}\n\n/**\n * IP-8: the `agents:invoke` scope required to cancel a run over the WS\n * transport — the same requirement the REST `POST /runs/:runId/abort` route\n * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).\n */\nconst INVOKE_SCOPE: ParsedScope = parseScope('agents:invoke');\n\n/**\n * IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.\n * `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}\n * gate on run cancellation.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\n\nfunction grantsInvokeScope(granted: ReadonlyArray<ParsedScope>): boolean {\n for (const scope of granted) {\n if (scopeMatches(scope, INVOKE_SCOPE)) return true;\n }\n return false;\n}\n\nfunction acceptTicket(ticket: WsTicket): ResolvedUpgradeAuth {\n return {\n kind: 'ok',\n tokenId: ticket.tokenId,\n grantedScopes: ticket.scopes,\n };\n}\n\nfunction mapSubscribeErrorCode(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n): number {\n switch (reason) {\n case 'scope-denied':\n return RPC_ERROR_CODES.SCOPE_DENIED;\n case 'subject-wildcard':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n case 'subject-unknown':\n return RPC_ERROR_CODES.METHOD_NOT_FOUND;\n case 'subject-malformed':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n }\n}\n\nfunction mapSubscribeErrorMessage(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n subject: string,\n): string {\n switch (reason) {\n case 'scope-denied':\n return `Token lacks required scope for subject '${subject}'.`;\n case 'subject-wildcard':\n return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;\n case 'subject-unknown':\n return `Unknown subject grammar: '${subject}'.`;\n case 'subject-malformed':\n return `Subject '${subject}' is malformed.`;\n }\n}\n\nfunction defaultSubscriberId(): string {\n return `sub-conn-${randomToken(8)}`;\n}\n\nfunction defaultSubscriptionId(subscriberId: string): string {\n return `${subscriberId}-${randomToken(6)}`;\n}\n\nfunction randomToken(length: number): string {\n const bytes = new Uint8Array(length);\n if (\n typeof globalThis.crypto !== 'undefined' &&\n typeof globalThis.crypto.getRandomValues === 'function'\n ) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n return Buffer.from(bytes).toString('hex');\n}\n\nvoid SUBPROTOCOL_NAME;\n"],"mappings":"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"}
1
+ {"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","frame: ServerMessage","INVOKE_SCOPE: ParsedScope","ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope>"],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":["/**\n * Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter\n * to the dispatcher: validates the subprotocol, runs auth (bearer\n * via `requireAuth` middleware OR ticket via the in-memory store),\n * and bridges the per-connection `WSContext` callbacks to the\n * dispatcher's RPC handler.\n *\n * ## Subprotocol + browser ticket flow\n *\n * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}\n * (`graphorin.protocol.v1`) - the canonical wire contract lives in\n * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:\n *\n * - **Bearer (non-browser):** the client sends `Authorization:\n * Bearer <token>` and a single `Sec-WebSocket-Protocol:\n * graphorin.protocol.v1` token.\n * - **Ticket (browser):** the `WebSocket` constructor cannot set\n * headers, so the browser client offers two subprotocol tokens -\n * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes\n * back only the canonical name (in `app.ts`'s `handleProtocols`),\n * and {@link resolveUpgradeAuth} extracts the ticket via\n * `parseTicketSubprotocol` and redeems it against the single-use\n * {@link WsTicketStore}. Tickets are short-lived and one-shot to\n * bound replay.\n *\n * @packageDocumentation\n */\n\nimport {\n ClientMessageSchema,\n closeCodeFor,\n isCancelledNotification,\n isInitializeRequest,\n isPingRequest,\n isRunCancelRequest,\n isSubscribeRequest,\n isUnsubscribeRequest,\n negotiateSubprotocol,\n parseTicketSubprotocol,\n RPC_ERROR_CODES,\n type ServerMessage,\n SUBPROTOCOL_NAME,\n} from '@graphorin/protocol';\nimport type { ParsedScope, TokenVerifier } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { Context } from 'hono';\nimport type { WSContext, WSEvents } from 'hono/ws';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\nimport type { WsDispatcher } from './dispatcher.js';\nimport type { WsTicket, WsTicketStore } from './ticket.js';\n\n/**\n * Public configuration accepted by {@link createWsUpgradeEvents}.\n *\n * @stable\n */\nexport interface WsUpgradeOptions {\n readonly dispatcher: WsDispatcher;\n readonly tickets: WsTicketStore;\n /**\n * Token verifier for bearer / ticket upgrades. Optional only in the\n * IP-13 no-auth loopback mode (`anonymous: true`), where there is no\n * verifier to construct and every upgrade is accepted.\n */\n readonly verifier?: TokenVerifier;\n /**\n * IP-13: authentication is disabled server-wide (`auth.kind='none'`).\n * Accept the upgrade unconditionally with a full (`admin:*`) scope grant\n * instead of silently refusing to mount the WS route. Trusted-loopback\n * / single-operator mode only.\n */\n readonly anonymous?: boolean;\n readonly runs?: RunStateTracker;\n readonly now?: () => number;\n /**\n * Subprotocol the server advertises. Defaults to\n * {@link SUBPROTOCOL_NAME}; tests can override to exercise the\n * mismatch path.\n */\n readonly serverSubprotocol?: string;\n readonly newSubscriptionId?: () => string;\n readonly newSubscriberId?: () => string;\n}\n\n/**\n * Build the `WSEvents` callback bag the Hono helper consumes. The\n * function takes the request `Context` so the upgrade can read the\n * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.\n *\n * Production wiring on `@hono/node-ws`:\n *\n * ```ts\n * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });\n * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));\n * injectWebSocket(serve(...));\n * ```\n *\n * @stable\n */\nexport async function createWsUpgradeEvents(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<WSEvents> {\n const negotiated = negotiateSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (negotiated === null) {\n return rejectImmediately(closeCodeFor('protocol.violation'), 'protocol.violation');\n }\n const auth = await resolveUpgradeAuth(c, options);\n if (auth.kind === 'denied') {\n return rejectImmediately(closeCodeFor(auth.reason), auth.reason);\n }\n\n const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();\n const newSubscriptionId =\n options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));\n\n let unregister: (() => void) | undefined;\n let initialized = false;\n\n return {\n onOpen: (_event, ws) => {\n const handle = {\n id: subscriberId,\n tokenId: auth.tokenId,\n grantedScopes: auth.grantedScopes,\n send: (frame: ServerMessage) => {\n ws.send(JSON.stringify(frame));\n },\n close: (code: number, reason: string) => {\n ws.close(code, reason);\n },\n // IP-9: surface the socket's real backlog so the dispatcher's\n // backpressure threshold measures something - the synchronous\n // outstanding-events counter never accumulates.\n bufferedAmount: () => {\n const raw = (ws as { raw?: { bufferedAmount?: number } }).raw;\n return raw?.bufferedAmount ?? 0;\n },\n };\n const reg = options.dispatcher.registerSubscriber(handle);\n unregister = reg.unregister;\n },\n onMessage: (event, ws) => {\n const raw = typeof event.data === 'string' ? event.data : '';\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n sendRpcError(ws, undefined, RPC_ERROR_CODES.PARSE_ERROR, 'Frame is not valid JSON.');\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const parsed = ClientMessageSchema.safeParse(payload);\n if (!parsed.success) {\n sendRpcError(\n ws,\n (payload as { id?: string | number } | null)?.id,\n RPC_ERROR_CODES.INVALID_REQUEST,\n 'Frame failed schema validation.',\n );\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const message = parsed.data;\n if (isCancelledNotification(message)) {\n // IP-8: only an initialized connection holding `agents:invoke` may\n // cancel a run. A notification carries no id, so an unauthorised one\n // is silently ignored (there is no error channel) rather than aborting\n // an arbitrary run.\n if (initialized && grantsInvokeScope(auth.grantedScopes)) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\n return;\n }\n if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {\n // IP-21: a frame before `initialize` is a protocol-sequencing error,\n // not an auth failure - the connection is already authenticated.\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.PROTOCOL_VIOLATION,\n 'Send `initialize` before any other RPC.',\n );\n return;\n }\n if (isInitializeRequest(message)) {\n initialized = true;\n sendRpcSuccess(ws, message.id, {\n serverInfo: {\n name: 'graphorin-server',\n version: '0.6.0',\n },\n capabilities: {\n subscriptions: true,\n replayBuffer: true,\n sseFallback: true,\n },\n });\n return;\n }\n if (isPingRequest(message)) {\n const nonce = message.params?.nonce;\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'pong',\n ...(nonce !== undefined ? { nonce } : {}),\n } satisfies ServerMessage),\n );\n sendRpcSuccess(ws, message.id, { ok: true });\n return;\n }\n if (isSubscribeRequest(message)) {\n const subscriptionId = newSubscriptionId();\n const result = options.dispatcher.subscribe({\n subscriberId,\n subject: message.params.subject,\n subscriptionId,\n ...(message.params.sinceEventId !== undefined\n ? { sinceEventId: message.params.sinceEventId }\n : {}),\n });\n if (!result.ok) {\n sendRpcError(\n ws,\n message.id,\n mapSubscribeErrorCode(result.reason),\n mapSubscribeErrorMessage(result.reason, message.params.subject),\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId,\n subject: message.params.subject,\n snapshotEventId: result.snapshotEventId,\n replayedCount: result.replayedCount,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'subscribed',\n subscriptionId,\n subject: message.params.subject,\n ...(result.snapshotEventId !== undefined\n ? { snapshotEventId: result.snapshotEventId }\n : {}),\n } satisfies ServerMessage),\n );\n return;\n }\n if (isUnsubscribeRequest(message)) {\n const removed = options.dispatcher.unsubscribe(message.params.subscriptionId);\n if (!removed) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,\n `Subscription '${message.params.subscriptionId}' not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId: message.params.subscriptionId,\n unsubscribed: true,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'unsubscribed',\n subscriptionId: message.params.subscriptionId,\n } satisfies ServerMessage),\n );\n return;\n }\n if (isRunCancelRequest(message)) {\n // IP-8: mirror the REST `POST /runs/:runId/abort` scope gate so a\n // bearer token cannot abort an arbitrary run without `agents:invoke`.\n if (!grantsInvokeScope(auth.grantedScopes)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n \"Token lacks required scope 'agents:invoke'.\",\n );\n return;\n }\n const runId = message.params.runId;\n const aborted = options.runs?.abort(runId, message.params.reason ?? 'rpc-cancel') === true;\n if (!aborted) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.RUN_NOT_FOUND,\n `Run '${runId}' is not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n runId,\n cancelled: true,\n partialStateAvailable: true,\n });\n return;\n }\n },\n onClose: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n onError: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n };\n}\n\nfunction sendRpcSuccess(ws: WSContext, id: string | number, result: unknown): void {\n const frame: ServerMessage = { v: '1', jsonrpc: '2.0', id, result };\n ws.send(JSON.stringify(frame));\n}\n\nfunction sendRpcError(\n ws: WSContext,\n id: string | number | undefined,\n code: number,\n message: string,\n): void {\n const frame: ServerMessage = {\n v: '1',\n jsonrpc: '2.0',\n id: id ?? 0,\n error: { code, message },\n };\n ws.send(JSON.stringify(frame));\n}\n\nfunction rejectImmediately(code: number, reason: string): WSEvents {\n return {\n onOpen: (_event, ws) => {\n ws.close(code, reason);\n },\n };\n}\n\ninterface ResolvedUpgradeAuthOk {\n readonly kind: 'ok';\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n}\n\ninterface ResolvedUpgradeAuthDenied {\n readonly kind: 'denied';\n readonly reason: 'auth.required' | 'auth.invalid' | 'auth.scope_denied';\n}\n\ntype ResolvedUpgradeAuth = ResolvedUpgradeAuthOk | ResolvedUpgradeAuthDenied;\n\nasync function resolveUpgradeAuth(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<ResolvedUpgradeAuth> {\n // Phase 14a's HTTP middleware may have already verified the\n // bearer token (`requireAuth` runs before the upgrade route on\n // the authenticated subtree); if so, reuse the result.\n const state = c.var.state;\n if (state?.auth?.kind === 'token') {\n return {\n kind: 'ok',\n tokenId: state.auth.token.tokenId,\n grantedScopes: state.auth.grantedScopes,\n };\n }\n // IP-13: no-auth loopback mode - accept the upgrade with a full scope grant\n // instead of silently failing to mount. Checked before ticket/bearer so the\n // verifier may legitimately be absent.\n if (options.anonymous === true) {\n return { kind: 'ok', tokenId: 'anonymous', grantedScopes: ANONYMOUS_WS_SCOPES };\n }\n const ticketValue = parseTicketSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (ticketValue !== undefined) {\n const consumed = options.tickets.consume(ticketValue);\n if (!consumed.ok) {\n return { kind: 'denied', reason: 'auth.invalid' };\n }\n return acceptTicket(consumed.ticket);\n }\n const header = c.req.header('authorization') ?? c.req.header('Authorization');\n if (header?.toLowerCase().startsWith('bearer ') === true) {\n if (options.verifier === undefined) return { kind: 'denied', reason: 'auth.required' };\n const token = header.slice(7).trim();\n // P-05: pass the client IP like the HTTP auth middleware does, so\n // the verifier's per-IP failure threshold / lockout engages for\n // upgrade attempts too - without it `GET /v1/ws` was a\n // lockout-free brute-force surface.\n const ip = c.get('state')?.clientIp;\n const verified = await options.verifier.verify(token, ip !== undefined ? { ip } : {});\n if (!verified.ok) return { kind: 'denied', reason: 'auth.invalid' };\n return {\n kind: 'ok',\n tokenId: verified.token.tokenId,\n grantedScopes: verified.token.scopes,\n };\n }\n return { kind: 'denied', reason: 'auth.required' };\n}\n\n/**\n * IP-8: the `agents:invoke` scope required to cancel a run over the WS\n * transport - the same requirement the REST `POST /runs/:runId/abort` route\n * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).\n */\nconst INVOKE_SCOPE: ParsedScope = parseScope('agents:invoke');\n\n/**\n * IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.\n * `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}\n * gate on run cancellation.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\n\nfunction grantsInvokeScope(granted: ReadonlyArray<ParsedScope>): boolean {\n for (const scope of granted) {\n if (scopeMatches(scope, INVOKE_SCOPE)) return true;\n }\n return false;\n}\n\nfunction acceptTicket(ticket: WsTicket): ResolvedUpgradeAuth {\n return {\n kind: 'ok',\n tokenId: ticket.tokenId,\n grantedScopes: ticket.scopes,\n };\n}\n\nfunction mapSubscribeErrorCode(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n): number {\n switch (reason) {\n case 'scope-denied':\n return RPC_ERROR_CODES.SCOPE_DENIED;\n case 'subject-wildcard':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n case 'subject-unknown':\n return RPC_ERROR_CODES.METHOD_NOT_FOUND;\n case 'subject-malformed':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n }\n}\n\nfunction mapSubscribeErrorMessage(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n subject: string,\n): string {\n switch (reason) {\n case 'scope-denied':\n return `Token lacks required scope for subject '${subject}'.`;\n case 'subject-wildcard':\n return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;\n case 'subject-unknown':\n return `Unknown subject grammar: '${subject}'.`;\n case 'subject-malformed':\n return `Subject '${subject}' is malformed.`;\n }\n}\n\nfunction defaultSubscriberId(): string {\n return `sub-conn-${randomToken(8)}`;\n}\n\nfunction defaultSubscriptionId(subscriberId: string): string {\n return `${subscriberId}-${randomToken(6)}`;\n}\n\nfunction randomToken(length: number): string {\n const bytes = new Uint8Array(length);\n if (\n typeof globalThis.crypto !== 'undefined' &&\n typeof globalThis.crypto.getRandomValues === 'function'\n ) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n return Buffer.from(bytes).toString('hex');\n}\n\nvoid SUBPROTOCOL_NAME;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/server",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Standalone server runtime for the Graphorin framework: createServer({...}) factory, Hono HTTP app, REST endpoints (agents / workflows / sessions / memory / skills / mcp / tokens / audit / health / health/secrets / metrics / triggers / runs/:runId/replay / sessions/:id/replay / audit/verify), per-request authentication + scope middleware (HMAC-SHA256 + pepper, server pepper required), brute-force lockout, IETF draft-07 Idempotency-Key middleware with SQLite backing + LRU read cache, deny-by-default CORS, double-submit CSRF for browser flows, sliding-window rate limit, audit middleware that streams every authenticated request through the tamper-evident hash chain, programmatic AgentRegistry / WorkflowRegistry, lifecycle hooks (beforeStart / onReady / beforeShutdown), pre-bind secrets resolution + storage migration runner that fails fast on missing pepper / unresolvable SecretRef / missing encryption peer, graceful SIGTERM drain with state preservation, durable triggers daemon (cron / interval / idle / event survive process restart per the catch-up policy contract), consolidator daemon lifecycle integration with hard stop timeout, per-subsystem health rollup (storage / embedder / secrets / encryption / consolidator / triggers / replay-buffer), Prometheus text exposition `/v1/metrics` with the canonical graphorin_* metric inventory, scope-enforced replay endpoints (sanitized by default, raw requires `traces:read:raw` admin scope, every invocation appended to the audit chain), and `POST /v1/audit/verify` chain integrity check. Created and maintained by Oleksiy Stepurenko.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -103,20 +103,20 @@
103
103
  "dependencies": {
104
104
  "@hono/node-server": "^1.19.0",
105
105
  "@hono/node-ws": "^1.3.0",
106
- "hono": "^4.12.21",
106
+ "hono": "^4.12.25",
107
107
  "zod": "^3.25.0",
108
- "@graphorin/core": "0.5.0",
109
- "@graphorin/observability": "0.5.0",
110
- "@graphorin/protocol": "0.5.0",
111
- "@graphorin/security": "0.5.0",
112
- "@graphorin/store-sqlite": "0.5.0",
113
- "@graphorin/triggers": "0.5.0"
108
+ "@graphorin/core": "0.6.0",
109
+ "@graphorin/observability": "0.6.0",
110
+ "@graphorin/protocol": "0.6.0",
111
+ "@graphorin/security": "0.6.0",
112
+ "@graphorin/store-sqlite": "0.6.0",
113
+ "@graphorin/triggers": "0.6.0"
114
114
  },
115
115
  "peerDependencies": {
116
- "@graphorin/agent": "0.5.0",
117
- "@graphorin/memory": "0.5.0",
118
- "@graphorin/sessions": "0.5.0",
119
- "@graphorin/workflow": "0.5.0"
116
+ "@graphorin/agent": ">=0.5.0 <1.0.0",
117
+ "@graphorin/memory": ">=0.5.0 <1.0.0",
118
+ "@graphorin/sessions": ">=0.5.0 <1.0.0",
119
+ "@graphorin/workflow": ">=0.5.0 <1.0.0"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@graphorin/agent": {
@@ -137,14 +137,14 @@
137
137
  "provenance": true
138
138
  },
139
139
  "devDependencies": {
140
+ "@graphorin/agent": ">=0.6.0",
141
+ "@graphorin/memory": ">=0.6.0",
142
+ "@graphorin/sessions": ">=0.6.0",
143
+ "@graphorin/workflow": ">=0.6.0",
140
144
  "@types/better-sqlite3": "^7.6.13",
141
145
  "@types/ws": "^8.18.1",
142
146
  "better-sqlite3": "^12.9.0",
143
- "ws": "^8.20.1",
144
- "@graphorin/agent": "0.5.0",
145
- "@graphorin/memory": "0.5.0",
146
- "@graphorin/sessions": "0.5.0",
147
- "@graphorin/workflow": "0.5.0"
147
+ "ws": "^8.20.1"
148
148
  },
149
149
  "scripts": {
150
150
  "build": "tsdown",