@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":"audit.js","names":["filters: {\n limit?: number;\n fromSeq?: number;\n fromTs?: number;\n toTs?: number;\n action?: string;\n }","opts: { fromSeq?: number; toSeq?: number }","opts: { fromSeq?: number; toSeq?: number; format?: 'jsonl' | 'csv' }"],"sources":["../../src/routes/audit.ts"],"sourcesContent":["/**\n * Audit-log REST routes.\n *\n * GET /audit (scope `audit:read`)\n * POST /audit/export (scope `audit:export`)\n *\n * The handler delegates to a structurally-typed `AuditApi` so the\n * server code stays decoupled from any single audit backend.\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * @stable\n */\nexport interface AuditApi {\n list(opts: {\n readonly limit?: number;\n readonly fromSeq?: number;\n /** Inclusive lower bound on the entry timestamp (epoch ms). */\n readonly fromTs?: number;\n /** Inclusive upper bound on the entry timestamp (epoch ms). */\n readonly toTs?: number;\n /** Restrict to entries whose `action` matches the supplied id. */\n readonly action?: string;\n }): Promise<ReadonlyArray<unknown>>;\n export(opts: {\n readonly fromSeq?: number;\n readonly toSeq?: number;\n readonly format?: 'jsonl' | 'csv';\n }): Promise<{ readonly bytes: number; readonly format?: 'jsonl' | 'csv' }>;\n /**\n * Verify the chain integrity of every audit row in the inclusive\n * range. Phase 14c surfaces this through `POST /v1/audit/verify`.\n * Optional — operators that opt out of the audit chain should\n * leave this method off.\n */\n verify?(opts: {\n readonly fromSeq?: number;\n readonly toSeq?: number;\n }): Promise<{ readonly ok: boolean; readonly count?: number; readonly brokenAt?: number }>;\n}\n\nconst ISO_OR_EPOCH = z.union([z.coerce.number().int().nonnegative(), z.string().min(1)]);\n\nconst ListQuerySchema = z\n .object({\n limit: z.coerce.number().int().positive().max(1000).optional(),\n fromSeq: z.coerce.number().int().nonnegative().optional(),\n from: ISO_OR_EPOCH.optional(),\n to: ISO_OR_EPOCH.optional(),\n action: z.string().min(1).max(256).optional(),\n })\n .strict();\n\nfunction parseTimestamp(input: unknown): number | undefined {\n if (input === undefined) return undefined;\n if (typeof input === 'number') return input;\n const parsed = Date.parse(String(input));\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nconst ExportBodySchema = z\n .object({\n fromSeq: z.number().int().nonnegative().optional(),\n toSeq: z.number().int().nonnegative().optional(),\n format: z.enum(['jsonl', 'csv']).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface AuditRoutesDeps {\n readonly audit: AuditApi;\n}\n\n/**\n * @stable\n */\nexport function createAuditRoutes(deps: AuditRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('audit:read'), async (c) => {\n const parsed = ListQuerySchema.safeParse({\n limit: c.req.query('limit'),\n fromSeq: c.req.query('fromSeq'),\n from: c.req.query('from'),\n to: c.req.query('to'),\n action: c.req.query('action'),\n });\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid audit query.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const fromTs = parseTimestamp(parsed.data.from);\n const toTs = parseTimestamp(parsed.data.to);\n if (parsed.data.from !== undefined && fromTs === undefined) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Invalid 'from' timestamp: '${String(parsed.data.from)}' is not parseable.`,\n },\n 400,\n );\n }\n if (parsed.data.to !== undefined && toTs === undefined) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Invalid 'to' timestamp: '${String(parsed.data.to)}' is not parseable.`,\n },\n 400,\n );\n }\n const filters: {\n limit?: number;\n fromSeq?: number;\n fromTs?: number;\n toTs?: number;\n action?: string;\n } = {};\n if (parsed.data.limit !== undefined) filters.limit = parsed.data.limit;\n if (parsed.data.fromSeq !== undefined) filters.fromSeq = parsed.data.fromSeq;\n if (fromTs !== undefined) filters.fromTs = fromTs;\n if (toTs !== undefined) filters.toTs = toTs;\n if (parsed.data.action !== undefined) filters.action = parsed.data.action;\n const entries = await deps.audit.list(filters);\n return c.json({ entries });\n });\n\n app.post('/verify', createScopeMiddleware('audit:verify'), async (c) => {\n if (deps.audit.verify === undefined) {\n return c.json(\n {\n error: 'not-implemented',\n message: 'Audit chain verification is not enabled on this deployment.',\n },\n 501,\n );\n }\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid verify body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const opts: { fromSeq?: number; toSeq?: number } = {};\n if (parsed.data.fromSeq !== undefined) opts.fromSeq = parsed.data.fromSeq;\n if (parsed.data.toSeq !== undefined) opts.toSeq = parsed.data.toSeq;\n const result = await deps.audit.verify(opts);\n return c.json({ verify: result });\n });\n\n app.post('/export', createScopeMiddleware('audit:export'), async (c) => {\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid export body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const queryFormat = c.req.query('format');\n const requestedFormat =\n parsed.data.format ??\n (queryFormat === 'csv' ? 'csv' : queryFormat === 'jsonl' ? 'jsonl' : undefined);\n const opts: { fromSeq?: number; toSeq?: number; format?: 'jsonl' | 'csv' } = {};\n if (parsed.data.fromSeq !== undefined) opts.fromSeq = parsed.data.fromSeq;\n if (parsed.data.toSeq !== undefined) opts.toSeq = parsed.data.toSeq;\n if (requestedFormat !== undefined) opts.format = requestedFormat;\n const result = await deps.audit.export(opts);\n return c.json({ export: result });\n });\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"audit.js","names":["filters: {\n limit?: number;\n fromSeq?: number;\n fromTs?: number;\n toTs?: number;\n action?: string;\n }","opts: { fromSeq?: number; toSeq?: number }","opts: { fromSeq?: number; toSeq?: number; format?: 'jsonl' | 'csv' }"],"sources":["../../src/routes/audit.ts"],"sourcesContent":["/**\n * Audit-log REST routes.\n *\n * GET /audit (scope `audit:read`)\n * POST /audit/export (scope `audit:export`)\n *\n * The handler delegates to a structurally-typed `AuditApi` so the\n * server code stays decoupled from any single audit backend.\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * @stable\n */\nexport interface AuditApi {\n list(opts: {\n readonly limit?: number;\n readonly fromSeq?: number;\n /** Inclusive lower bound on the entry timestamp (epoch ms). */\n readonly fromTs?: number;\n /** Inclusive upper bound on the entry timestamp (epoch ms). */\n readonly toTs?: number;\n /** Restrict to entries whose `action` matches the supplied id. */\n readonly action?: string;\n }): Promise<ReadonlyArray<unknown>>;\n export(opts: {\n readonly fromSeq?: number;\n readonly toSeq?: number;\n readonly format?: 'jsonl' | 'csv';\n }): Promise<{ readonly bytes: number; readonly format?: 'jsonl' | 'csv' }>;\n /**\n * Verify the chain integrity of every audit row in the inclusive\n * range. Phase 14c surfaces this through `POST /v1/audit/verify`.\n * Optional - operators that opt out of the audit chain should\n * leave this method off.\n */\n verify?(opts: {\n readonly fromSeq?: number;\n readonly toSeq?: number;\n }): Promise<{ readonly ok: boolean; readonly count?: number; readonly brokenAt?: number }>;\n}\n\nconst ISO_OR_EPOCH = z.union([z.coerce.number().int().nonnegative(), z.string().min(1)]);\n\nconst ListQuerySchema = z\n .object({\n limit: z.coerce.number().int().positive().max(1000).optional(),\n fromSeq: z.coerce.number().int().nonnegative().optional(),\n from: ISO_OR_EPOCH.optional(),\n to: ISO_OR_EPOCH.optional(),\n action: z.string().min(1).max(256).optional(),\n })\n .strict();\n\nfunction parseTimestamp(input: unknown): number | undefined {\n if (input === undefined) return undefined;\n if (typeof input === 'number') return input;\n const parsed = Date.parse(String(input));\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nconst ExportBodySchema = z\n .object({\n fromSeq: z.number().int().nonnegative().optional(),\n toSeq: z.number().int().nonnegative().optional(),\n format: z.enum(['jsonl', 'csv']).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface AuditRoutesDeps {\n readonly audit: AuditApi;\n}\n\n/**\n * @stable\n */\nexport function createAuditRoutes(deps: AuditRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('audit:read'), async (c) => {\n const parsed = ListQuerySchema.safeParse({\n limit: c.req.query('limit'),\n fromSeq: c.req.query('fromSeq'),\n from: c.req.query('from'),\n to: c.req.query('to'),\n action: c.req.query('action'),\n });\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid audit query.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const fromTs = parseTimestamp(parsed.data.from);\n const toTs = parseTimestamp(parsed.data.to);\n if (parsed.data.from !== undefined && fromTs === undefined) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Invalid 'from' timestamp: '${String(parsed.data.from)}' is not parseable.`,\n },\n 400,\n );\n }\n if (parsed.data.to !== undefined && toTs === undefined) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Invalid 'to' timestamp: '${String(parsed.data.to)}' is not parseable.`,\n },\n 400,\n );\n }\n const filters: {\n limit?: number;\n fromSeq?: number;\n fromTs?: number;\n toTs?: number;\n action?: string;\n } = {};\n if (parsed.data.limit !== undefined) filters.limit = parsed.data.limit;\n if (parsed.data.fromSeq !== undefined) filters.fromSeq = parsed.data.fromSeq;\n if (fromTs !== undefined) filters.fromTs = fromTs;\n if (toTs !== undefined) filters.toTs = toTs;\n if (parsed.data.action !== undefined) filters.action = parsed.data.action;\n const entries = await deps.audit.list(filters);\n return c.json({ entries });\n });\n\n app.post('/verify', createScopeMiddleware('audit:verify'), async (c) => {\n if (deps.audit.verify === undefined) {\n return c.json(\n {\n error: 'not-implemented',\n message: 'Audit chain verification is not enabled on this deployment.',\n },\n 501,\n );\n }\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid verify body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const opts: { fromSeq?: number; toSeq?: number } = {};\n if (parsed.data.fromSeq !== undefined) opts.fromSeq = parsed.data.fromSeq;\n if (parsed.data.toSeq !== undefined) opts.toSeq = parsed.data.toSeq;\n const result = await deps.audit.verify(opts);\n return c.json({ verify: result });\n });\n\n app.post('/export', createScopeMiddleware('audit:export'), async (c) => {\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid export body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const queryFormat = c.req.query('format');\n const requestedFormat =\n parsed.data.format ??\n (queryFormat === 'csv' ? 'csv' : queryFormat === 'jsonl' ? 'jsonl' : undefined);\n const opts: { fromSeq?: number; toSeq?: number; format?: 'jsonl' | 'csv' } = {};\n if (parsed.data.fromSeq !== undefined) opts.fromSeq = parsed.data.fromSeq;\n if (parsed.data.toSeq !== undefined) opts.toSeq = parsed.data.toSeq;\n if (requestedFormat !== undefined) opts.format = requestedFormat;\n const result = await deps.audit.export(opts);\n return c.json({ export: result });\n });\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../../src/routes/auth.ts"],"sourcesContent":["/**\n * Auth-related REST routes that exist purely to support the WebSocket\n * surface in `@graphorin/server/ws`.\n *\n * POST /session/ws-ticket (scope `agents:invoke`)\n *\n * Browser clients call the endpoint with a regular HTTP `Authorization:\n * Bearer <token>` header to mint a single-use, short-lived ticket; the\n * ticket value is then attached to the WebSocket upgrade request as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`). The\n * server-side WS upgrade handler (see `../ws/upgrade.ts`) consumes the\n * ticket against the same in-memory store so the round-trip is\n * race-free.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsTicketStore } from '../ws/ticket.js';\n\n/**\n * Stable shape consumed by {@link createAuthRoutes}.\n *\n * @stable\n */\nexport interface AuthRoutesDeps {\n readonly tickets: WsTicketStore;\n}\n\n/**\n * Build the auth router. The router is mounted at the same base path\n * as the rest of the REST surface (defaults to `/v1`).\n *\n * @stable\n */\nexport function createAuthRoutes(deps: AuthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.post('/session/ws-ticket', createScopeMiddleware('agents:invoke'), (c) => {\n const auth = c.get('state').auth;\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // bearer token, but the anonymous principal is fully authorized mint a\n // ticket bound to a synthetic `anonymous` id so a browser client written\n // for token mode still completes the round-trip.\n const principal =\n auth.kind === 'token'\n ? { tokenId: auth.token.tokenId, scopes: auth.grantedScopes }\n : auth.kind === 'anonymous'\n ? { tokenId: 'anonymous', scopes: auth.grantedScopes }\n : undefined;\n if (principal === undefined) {\n return c.json(\n {\n error: 'auth-required',\n message: 'Bearer token required to mint a WS ticket.',\n },\n 401,\n );\n }\n const ticket = deps.tickets.issue(principal);\n return c.json(\n {\n ticket: ticket.value,\n expiresAt: ticket.expiresAt,\n ttlMs: deps.tickets.ttlMs,\n },\n 201,\n );\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,iBAAiB,MAA4D;CAC3F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,KAAK,sBAAsB,sBAAsB,gBAAgB,GAAG,MAAM;EAC5E,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAK5B,MAAM,YACJ,KAAK,SAAS,UACV;GAAE,SAAS,KAAK,MAAM;GAAS,QAAQ,KAAK;GAAe,GAC3D,KAAK,SAAS,cACZ;GAAE,SAAS;GAAa,QAAQ,KAAK;GAAe,GACpD;AACR,MAAI,cAAc,OAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,SAAS,KAAK,QAAQ,MAAM,UAAU;AAC5C,SAAO,EAAE,KACP;GACE,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,OAAO,KAAK,QAAQ;GACrB,EACD,IACD;GACD;AAEF,QAAO"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../../src/routes/auth.ts"],"sourcesContent":["/**\n * Auth-related REST routes that exist purely to support the WebSocket\n * surface in `@graphorin/server/ws`.\n *\n * POST /session/ws-ticket (scope `agents:invoke`)\n *\n * Browser clients call the endpoint with a regular HTTP `Authorization:\n * Bearer <token>` header to mint a single-use, short-lived ticket; the\n * ticket value is then attached to the WebSocket upgrade request as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`). The\n * server-side WS upgrade handler (see `../ws/upgrade.ts`) consumes the\n * ticket against the same in-memory store so the round-trip is\n * race-free.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsTicketStore } from '../ws/ticket.js';\n\n/**\n * Stable shape consumed by {@link createAuthRoutes}.\n *\n * @stable\n */\nexport interface AuthRoutesDeps {\n readonly tickets: WsTicketStore;\n}\n\n/**\n * Build the auth router. The router is mounted at the same base path\n * as the rest of the REST surface (defaults to `/v1`).\n *\n * @stable\n */\nexport function createAuthRoutes(deps: AuthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.post('/session/ws-ticket', createScopeMiddleware('agents:invoke'), (c) => {\n const auth = c.get('state').auth;\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // bearer token, but the anonymous principal is fully authorized - mint a\n // ticket bound to a synthetic `anonymous` id so a browser client written\n // for token mode still completes the round-trip.\n const principal =\n auth.kind === 'token'\n ? { tokenId: auth.token.tokenId, scopes: auth.grantedScopes }\n : auth.kind === 'anonymous'\n ? { tokenId: 'anonymous', scopes: auth.grantedScopes }\n : undefined;\n if (principal === undefined) {\n return c.json(\n {\n error: 'auth-required',\n message: 'Bearer token required to mint a WS ticket.',\n },\n 401,\n );\n }\n const ticket = deps.tickets.issue(principal);\n return c.json(\n {\n ticket: ticket.value,\n expiresAt: ticket.expiresAt,\n ttlMs: deps.tickets.ttlMs,\n },\n 201,\n );\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,iBAAiB,MAA4D;CAC3F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,KAAK,sBAAsB,sBAAsB,gBAAgB,GAAG,MAAM;EAC5E,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAK5B,MAAM,YACJ,KAAK,SAAS,UACV;GAAE,SAAS,KAAK,MAAM;GAAS,QAAQ,KAAK;GAAe,GAC3D,KAAK,SAAS,cACZ;GAAE,SAAS;GAAa,QAAQ,KAAK;GAAe,GACpD;AACR,MAAI,cAAc,OAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,SAAS,KAAK,QAAQ,MAAM,UAAU;AAC5C,SAAO,EAAE,KACP;GACE,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,OAAO,KAAK,QAAQ;GACrB,EACD,IACD;GACD;AAEF,QAAO"}
@@ -2,7 +2,7 @@ import { Hono } from "hono";
2
2
 
3
3
  //#region src/routes/health.ts
4
4
  /**
5
- * `GET /v1/health` minimal health endpoint kept for backward
5
+ * `GET /v1/health` - minimal health endpoint kept for backward
6
6
  * compatibility with consumers that wired the Phase 14a route
7
7
  * factory directly. The active server now mounts the extended
8
8
  * Phase 14c routes from `@graphorin/server/health`. New code should
@@ -1 +1 @@
1
- {"version":3,"file":"health.js","names":[],"sources":["../../src/routes/health.ts"],"sourcesContent":["/**\n * `GET /v1/health` minimal health endpoint kept for backward\n * compatibility with consumers that wired the Phase 14a route\n * factory directly. The active server now mounts the extended\n * Phase 14c routes from `@graphorin/server/health`. New code should\n * use {@link import('../health/routes.js').createExtendedHealthRoutes}.\n *\n * @deprecated Phase 14c. Use `createExtendedHealthRoutes` from\n * `@graphorin/server/health` instead. This export\n * stays available so existing consumers do not break.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * @stable\n */\nexport interface HealthRoutesDeps {\n readonly version: string;\n readonly startedAt: number;\n readonly now?: () => number;\n}\n\n/**\n * @stable\n */\nexport function createHealthRoutes(deps: HealthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const now = deps.now ?? Date.now;\n app.get('/', (c) =>\n c.json({\n status: 'ok' as const,\n version: deps.version,\n uptimeSeconds: Math.max(0, Math.floor((now() - deps.startedAt) / 1000)),\n }),\n );\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;CACtD,MAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,KAAI,IAAI,MAAM,MACZ,EAAE,KAAK;EACL,QAAQ;EACR,SAAS,KAAK;EACd,eAAe,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,GAAG,KAAK,aAAa,IAAK,CAAC;EACxE,CAAC,CACH;AACD,QAAO"}
1
+ {"version":3,"file":"health.js","names":[],"sources":["../../src/routes/health.ts"],"sourcesContent":["/**\n * `GET /v1/health` - minimal health endpoint kept for backward\n * compatibility with consumers that wired the Phase 14a route\n * factory directly. The active server now mounts the extended\n * Phase 14c routes from `@graphorin/server/health`. New code should\n * use {@link import('../health/routes.js').createExtendedHealthRoutes}.\n *\n * @deprecated Phase 14c. Use `createExtendedHealthRoutes` from\n * `@graphorin/server/health` instead. This export\n * stays available so existing consumers do not break.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * @stable\n */\nexport interface HealthRoutesDeps {\n readonly version: string;\n readonly startedAt: number;\n readonly now?: () => number;\n}\n\n/**\n * @stable\n */\nexport function createHealthRoutes(deps: HealthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const now = deps.now ?? Date.now;\n app.get('/', (c) =>\n c.json({\n status: 'ok' as const,\n version: deps.version,\n uptimeSeconds: Math.max(0, Math.floor((now() - deps.startedAt) / 1000)),\n }),\n );\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;CACtD,MAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,KAAI,IAAI,MAAM,MACZ,EAAE,KAAK;EACL,QAAQ;EACR,SAAS,KAAK;EACd,eAAe,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,GAAG,KAAK,aAAa,IAAK,CAAC;EACxE,CAAC,CACH;AACD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","names":["filter: { userId?: string; agentId?: string }","opts: { limit?: number }","exportOpts: { includeAuditEntries?: boolean; hash?: boolean }"],"sources":["../../src/routes/sessions.ts"],"sourcesContent":["/**\n * Session REST routes. Delegates to a structurally-typed `SessionApi`\n * supplied by the operator (in practice, the `@graphorin/sessions`\n * package's `SessionManager`); the contract is intentionally open so\n * tests can stub it without dragging the full sessions facade in.\n *\n * GET /sessions (scope `sessions:read`)\n * GET /sessions/:id (scope `sessions:read`)\n * POST /sessions (idempotent; scope `sessions:write`)\n * DELETE /sessions/:id (scope `sessions:write`)\n * GET /sessions/:id/messages (scope `sessions:read`)\n * GET /sessions/:id/handoffs (scope `sessions:read`)\n * POST /sessions/:id/export (scope `sessions:export`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * Minimal contract route handlers consume. Real deployments wire\n * `@graphorin/sessions.SessionManager` in directly; tests pass a\n * lighter stub.\n *\n * @stable\n */\nexport interface SessionApi {\n list(opts: {\n readonly userId?: string;\n readonly agentId?: string;\n }): Promise<ReadonlyArray<unknown>>;\n get(sessionId: string): Promise<unknown | null>;\n create(input: {\n readonly userId: string;\n readonly agentId: string;\n readonly sessionId?: string;\n readonly title?: string;\n readonly tags?: ReadonlyArray<string>;\n }): Promise<unknown>;\n remove(sessionId: string): Promise<boolean>;\n listMessages(\n sessionId: string,\n opts: { readonly limit?: number },\n ): Promise<ReadonlyArray<unknown>>;\n listHandoffs(sessionId: string): Promise<ReadonlyArray<unknown>>;\n exportSession?(\n sessionId: string,\n opts: {\n readonly includeAuditEntries?: boolean;\n readonly hash?: boolean;\n },\n ): Promise<unknown>;\n replaySession?(\n sessionId: string,\n opts: { readonly raw?: boolean; readonly fromMessageId?: string },\n ): AsyncIterable<unknown>;\n}\n\nconst CreateBodySchema = z\n .object({\n userId: z.string().min(1),\n agentId: z.string().min(1),\n sessionId: z.string().min(1).optional(),\n title: z.string().min(1).optional(),\n tags: z.array(z.string()).optional(),\n })\n .strict();\n\nconst MessagesQuerySchema = z\n .object({\n limit: z.coerce.number().int().positive().max(1000).optional(),\n })\n .strict();\n\nconst ExportBodySchema = z\n .object({\n includeAuditEntries: z.boolean().optional(),\n hash: z.boolean().optional(),\n })\n .strict()\n .default({});\n\nconst _ReplayBodySchema = z\n .object({\n raw: z.boolean().optional(),\n fromMessageId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface SessionRoutesDeps {\n readonly sessions: SessionApi;\n}\n\n/**\n * @stable\n */\nexport function createSessionRoutes(deps: SessionRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('sessions:read'), async (c) => {\n const userId = c.req.query('userId');\n const agentId = c.req.query('agentId');\n const filter: { userId?: string; agentId?: string } = {};\n if (userId !== undefined && userId.length > 0) filter.userId = userId;\n if (agentId !== undefined && agentId.length > 0) filter.agentId = agentId;\n const list = await deps.sessions.list(filter);\n return c.json({ sessions: list });\n });\n\n app.post('/', createScopeMiddleware('sessions:write'), async (c) => {\n const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid session body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const session = await deps.sessions.create({\n userId: parsed.data.userId,\n agentId: parsed.data.agentId,\n ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),\n ...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),\n ...(parsed.data.tags !== undefined ? { tags: parsed.data.tags } : {}),\n });\n return c.json({ session }, 201);\n });\n\n app.get('/:id', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const session = await deps.sessions.get(id);\n if (session === null) {\n return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);\n }\n return c.json({ session });\n });\n\n app.delete('/:id', createScopeMiddleware('sessions:write'), async (c) => {\n const id = c.req.param('id');\n const removed = await deps.sessions.remove(id);\n if (!removed) {\n return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);\n }\n return c.body(null, 204);\n });\n\n app.get('/:id/messages', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const parsed = MessagesQuerySchema.safeParse({ limit: c.req.query('limit') });\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid messages query.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const opts: { limit?: number } = {};\n if (parsed.data.limit !== undefined) opts.limit = parsed.data.limit;\n const messages = await deps.sessions.listMessages(id, opts);\n return c.json({ sessionId: id, messages });\n });\n\n app.get('/:id/handoffs', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const handoffs = await deps.sessions.listHandoffs(id);\n return c.json({ sessionId: id, handoffs });\n });\n\n app.post('/:id/export', createScopeMiddleware('sessions:export'), async (c) => {\n const id = c.req.param('id');\n if (deps.sessions.exportSession === undefined) {\n return c.json(\n { error: 'session-export-unsupported', message: 'Export is not enabled.' },\n 400,\n );\n }\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid export body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const exportOpts: { includeAuditEntries?: boolean; hash?: boolean } = {};\n if (parsed.data.includeAuditEntries !== undefined) {\n exportOpts.includeAuditEntries = parsed.data.includeAuditEntries;\n }\n if (parsed.data.hash !== undefined) exportOpts.hash = parsed.data.hash;\n const result = await deps.sessions.exportSession(id, exportOpts);\n return c.json({ sessionId: id, export: result });\n });\n\n // IP-14: the session-replay route lives in `replay/routes.ts` (the\n // scope-laddered, audit-backed implementation). The Phase-14a stub\n // that lived here SHADOWED it (mounted earlier on the same path)\n // while advertising a `session:<id>/replay` subject the WS grammar\n // cannot parse — deleted.\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sessions.js","names":["filter: { userId?: string; agentId?: string }","opts: { limit?: number }","exportOpts: { includeAuditEntries?: boolean; hash?: boolean }"],"sources":["../../src/routes/sessions.ts"],"sourcesContent":["/**\n * Session REST routes. Delegates to a structurally-typed `SessionApi`\n * supplied by the operator (in practice, the `@graphorin/sessions`\n * package's `SessionManager`); the contract is intentionally open so\n * tests can stub it without dragging the full sessions facade in.\n *\n * GET /sessions (scope `sessions:read`)\n * GET /sessions/:id (scope `sessions:read`)\n * POST /sessions (idempotent; scope `sessions:write`)\n * DELETE /sessions/:id (scope `sessions:write`)\n * GET /sessions/:id/messages (scope `sessions:read`)\n * GET /sessions/:id/handoffs (scope `sessions:read`)\n * POST /sessions/:id/export (scope `sessions:export`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * Minimal contract route handlers consume. Real deployments wire\n * `@graphorin/sessions.SessionManager` in directly; tests pass a\n * lighter stub.\n *\n * @stable\n */\nexport interface SessionApi {\n list(opts: {\n readonly userId?: string;\n readonly agentId?: string;\n }): Promise<ReadonlyArray<unknown>>;\n get(sessionId: string): Promise<unknown | null>;\n create(input: {\n readonly userId: string;\n readonly agentId: string;\n readonly sessionId?: string;\n readonly title?: string;\n readonly tags?: ReadonlyArray<string>;\n }): Promise<unknown>;\n remove(sessionId: string): Promise<boolean>;\n listMessages(\n sessionId: string,\n opts: { readonly limit?: number },\n ): Promise<ReadonlyArray<unknown>>;\n listHandoffs(sessionId: string): Promise<ReadonlyArray<unknown>>;\n exportSession?(\n sessionId: string,\n opts: {\n readonly includeAuditEntries?: boolean;\n readonly hash?: boolean;\n },\n ): Promise<unknown>;\n replaySession?(\n sessionId: string,\n opts: { readonly raw?: boolean; readonly fromMessageId?: string },\n ): AsyncIterable<unknown>;\n}\n\nconst CreateBodySchema = z\n .object({\n userId: z.string().min(1),\n agentId: z.string().min(1),\n sessionId: z.string().min(1).optional(),\n title: z.string().min(1).optional(),\n tags: z.array(z.string()).optional(),\n })\n .strict();\n\nconst MessagesQuerySchema = z\n .object({\n limit: z.coerce.number().int().positive().max(1000).optional(),\n })\n .strict();\n\nconst ExportBodySchema = z\n .object({\n includeAuditEntries: z.boolean().optional(),\n hash: z.boolean().optional(),\n })\n .strict()\n .default({});\n\nconst _ReplayBodySchema = z\n .object({\n raw: z.boolean().optional(),\n fromMessageId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface SessionRoutesDeps {\n readonly sessions: SessionApi;\n}\n\n/**\n * @stable\n */\nexport function createSessionRoutes(deps: SessionRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('sessions:read'), async (c) => {\n const userId = c.req.query('userId');\n const agentId = c.req.query('agentId');\n const filter: { userId?: string; agentId?: string } = {};\n if (userId !== undefined && userId.length > 0) filter.userId = userId;\n if (agentId !== undefined && agentId.length > 0) filter.agentId = agentId;\n const list = await deps.sessions.list(filter);\n return c.json({ sessions: list });\n });\n\n app.post('/', createScopeMiddleware('sessions:write'), async (c) => {\n const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid session body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const session = await deps.sessions.create({\n userId: parsed.data.userId,\n agentId: parsed.data.agentId,\n ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),\n ...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),\n ...(parsed.data.tags !== undefined ? { tags: parsed.data.tags } : {}),\n });\n return c.json({ session }, 201);\n });\n\n app.get('/:id', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const session = await deps.sessions.get(id);\n if (session === null) {\n return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);\n }\n return c.json({ session });\n });\n\n app.delete('/:id', createScopeMiddleware('sessions:write'), async (c) => {\n const id = c.req.param('id');\n const removed = await deps.sessions.remove(id);\n if (!removed) {\n return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);\n }\n return c.body(null, 204);\n });\n\n app.get('/:id/messages', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const parsed = MessagesQuerySchema.safeParse({ limit: c.req.query('limit') });\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid messages query.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const opts: { limit?: number } = {};\n if (parsed.data.limit !== undefined) opts.limit = parsed.data.limit;\n const messages = await deps.sessions.listMessages(id, opts);\n return c.json({ sessionId: id, messages });\n });\n\n app.get('/:id/handoffs', createScopeMiddleware('sessions:read'), async (c) => {\n const id = c.req.param('id');\n const handoffs = await deps.sessions.listHandoffs(id);\n return c.json({ sessionId: id, handoffs });\n });\n\n app.post('/:id/export', createScopeMiddleware('sessions:export'), async (c) => {\n const id = c.req.param('id');\n if (deps.sessions.exportSession === undefined) {\n return c.json(\n { error: 'session-export-unsupported', message: 'Export is not enabled.' },\n 400,\n );\n }\n const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid export body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const exportOpts: { includeAuditEntries?: boolean; hash?: boolean } = {};\n if (parsed.data.includeAuditEntries !== undefined) {\n exportOpts.includeAuditEntries = parsed.data.includeAuditEntries;\n }\n if (parsed.data.hash !== undefined) exportOpts.hash = parsed.data.hash;\n const result = await deps.sessions.exportSession(id, exportOpts);\n return c.json({ sessionId: id, export: result });\n });\n\n // IP-14: the session-replay route lives in `replay/routes.ts` (the\n // scope-laddered, audit-backed implementation). The Phase-14a stub\n // that lived here SHADOWED it (mounted earlier on the same path)\n // while advertising a `session:<id>/replay` subject the WS grammar\n // cannot parse - deleted.\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","names":[],"sources":["../../src/routes/tokens.ts"],"sourcesContent":["/**\n * Token-management REST routes. The handlers wrap the\n * `@graphorin/security/auth` CRUD library functions so the operator\n * can mint / list / revoke tokens through the same API surface as\n * `graphorin token` (Phase 15).\n *\n * @packageDocumentation\n */\n\nimport type { AuthTokenStore } from '@graphorin/core/contracts';\nimport { createToken, listTokens, revokeToken, type SecretValue } from '@graphorin/security';\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * @stable\n */\nexport interface TokensRoutesDeps {\n readonly tokenStore: AuthTokenStore;\n readonly pepper: SecretValue;\n readonly defaultEnv: string;\n readonly allowedEnvs: ReadonlyArray<string>;\n}\n\nconst CreateBodySchema = z\n .object({\n label: z.string().min(1).optional(),\n scopes: z.array(z.string().min(3)).min(1),\n env: z.string().min(1).optional(),\n expiresInMs: z.number().int().positive().optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport function createTokensRoutes(deps: TokensRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('tokens:list'), async (c) => {\n const records = await listTokens(deps.tokenStore);\n // Never reveal hashes or pepper-derived material.\n const sanitized = records.map((r) => ({\n id: r.id,\n label: r.label,\n scopes: r.scopes,\n createdAt: r.createdAt,\n lastUsedAt: r.lastUsedAt,\n expiresAt: r.expiresAt,\n revokedAt: r.revokedAt,\n }));\n return c.json({ tokens: sanitized });\n });\n\n app.post('/', createScopeMiddleware('tokens:create'), async (c) => {\n const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid create-token body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const env = parsed.data.env ?? deps.defaultEnv;\n if (!deps.allowedEnvs.includes(env)) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Environment '${env}' is not in the allowed set.`,\n allowed: deps.allowedEnvs,\n },\n 400,\n );\n }\n const created = await createToken({\n tokenStore: deps.tokenStore,\n pepper: deps.pepper,\n env,\n scopes: parsed.data.scopes,\n ...(parsed.data.label !== undefined ? { label: parsed.data.label } : {}),\n ...(parsed.data.expiresInMs !== undefined ? { expiresInMs: parsed.data.expiresInMs } : {}),\n });\n // Reveal the raw token exactly once at creation.\n const raw = await created.raw.use((value) => value);\n return c.json(\n {\n token: {\n id: created.record.id,\n label: created.record.label,\n scopes: created.record.scopes,\n createdAt: created.record.createdAt,\n expiresAt: created.record.expiresAt,\n },\n raw,\n warning: 'Store this raw value securely; it is shown exactly once.',\n },\n 201,\n );\n });\n\n app.delete('/:id', createScopeMiddleware('tokens:revoke'), async (c) => {\n const id = c.req.param('id');\n const updated = await revokeToken(deps.tokenStore, id);\n if (updated === undefined) {\n return c.json({ error: 'token-not-found', message: `Token '${id}' not found.` }, 404);\n }\n return c.body(null, 204);\n });\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":";;;;;;AA4BA,MAAM,mBAAmB,EACtB,OAAO;CACN,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACnC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;CACzC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACjC,aAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU;CACpD,CAAC,CACD,QAAQ;;;;AAKX,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,IAAI,KAAK,sBAAsB,cAAc,EAAE,OAAO,MAAM;EAG9D,MAAM,aAFU,MAAM,WAAW,KAAK,WAAW,EAEvB,KAAK,OAAO;GACpC,IAAI,EAAE;GACN,OAAO,EAAE;GACT,QAAQ,EAAE;GACV,WAAW,EAAE;GACb,YAAY,EAAE;GACd,WAAW,EAAE;GACb,WAAW,EAAE;GACd,EAAE;AACH,SAAO,EAAE,KAAK,EAAE,QAAQ,WAAW,CAAC;GACpC;AAEF,KAAI,KAAK,KAAK,sBAAsB,gBAAgB,EAAE,OAAO,MAAM;EACjE,MAAM,SAAS,iBAAiB,UAAU,MAAM,gBAAgB,EAAE,CAAC;AACnE,MAAI,CAAC,OAAO,QACV,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACT,QAAQ,OAAO,MAAM,OAAO,KAAK,OAAO;IAAE,MAAM,EAAE;IAAM,SAAS,EAAE;IAAS,EAAE;GAC/E,EACD,IACD;EAEH,MAAM,MAAM,OAAO,KAAK,OAAO,KAAK;AACpC,MAAI,CAAC,KAAK,YAAY,SAAS,IAAI,CACjC,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS,gBAAgB,IAAI;GAC7B,SAAS,KAAK;GACf,EACD,IACD;EAEH,MAAM,UAAU,MAAM,YAAY;GAChC,YAAY,KAAK;GACjB,QAAQ,KAAK;GACb;GACA,QAAQ,OAAO,KAAK;GACpB,GAAI,OAAO,KAAK,UAAU,SAAY,EAAE,OAAO,OAAO,KAAK,OAAO,GAAG,EAAE;GACvE,GAAI,OAAO,KAAK,gBAAgB,SAAY,EAAE,aAAa,OAAO,KAAK,aAAa,GAAG,EAAE;GAC1F,CAAC;EAEF,MAAM,MAAM,MAAM,QAAQ,IAAI,KAAK,UAAU,MAAM;AACnD,SAAO,EAAE,KACP;GACE,OAAO;IACL,IAAI,QAAQ,OAAO;IACnB,OAAO,QAAQ,OAAO;IACtB,QAAQ,QAAQ,OAAO;IACvB,WAAW,QAAQ,OAAO;IAC1B,WAAW,QAAQ,OAAO;IAC3B;GACD;GACA,SAAS;GACV,EACD,IACD;GACD;AAEF,KAAI,OAAO,QAAQ,sBAAsB,gBAAgB,EAAE,OAAO,MAAM;EACtE,MAAM,KAAK,EAAE,IAAI,MAAM,KAAK;AAE5B,MADgB,MAAM,YAAY,KAAK,YAAY,GAAG,KACtC,OACd,QAAO,EAAE,KAAK;GAAE,OAAO;GAAmB,SAAS,UAAU,GAAG;GAAe,EAAE,IAAI;AAEvF,SAAO,EAAE,KAAK,MAAM,IAAI;GACxB;AAEF,QAAO;;AAGT,eAAe,gBAAgB,GAA8D;AAC3F,KAAI;AACF,SAAO,MAAM,EAAE,IAAI,MAAM;SACnB;AACN,SAAO,EAAE"}
1
+ {"version":3,"file":"tokens.js","names":[],"sources":["../../src/routes/tokens.ts"],"sourcesContent":["/**\n * Token-management REST routes. The handlers wrap the\n * `@graphorin/security/auth` CRUD library functions so the operator\n * can mint / list / revoke tokens through the same API surface as\n * `graphorin token` (Phase 15).\n *\n * @packageDocumentation\n */\n\nimport type { AuthTokenStore } from '@graphorin/core/contracts';\nimport { createToken, listTokens, revokeToken, type SecretValue } from '@graphorin/security';\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * @stable\n */\nexport interface TokensRoutesDeps {\n readonly tokenStore: AuthTokenStore;\n readonly pepper: SecretValue;\n readonly defaultEnv: string;\n readonly allowedEnvs: ReadonlyArray<string>;\n}\n\nconst CreateBodySchema = z\n .object({\n label: z.string().min(1).optional(),\n scopes: z.array(z.string().min(3)).min(1),\n env: z.string().min(1).optional(),\n expiresInMs: z.number().int().positive().optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport function createTokensRoutes(deps: TokensRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('tokens:list'), async (c) => {\n const records = await listTokens(deps.tokenStore);\n // Never reveal hashes or pepper-derived material.\n const sanitized = records.map((r) => ({\n id: r.id,\n label: r.label,\n scopes: r.scopes,\n createdAt: r.createdAt,\n lastUsedAt: r.lastUsedAt,\n expiresAt: r.expiresAt,\n revokedAt: r.revokedAt,\n }));\n return c.json({ tokens: sanitized });\n });\n\n app.post('/', createScopeMiddleware('tokens:create'), async (c) => {\n const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid create-token body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const env = parsed.data.env ?? deps.defaultEnv;\n if (!deps.allowedEnvs.includes(env)) {\n return c.json(\n {\n error: 'config-invalid',\n message: `Environment '${env}' is not in the allowed set.`,\n allowed: deps.allowedEnvs,\n },\n 400,\n );\n }\n const created = await createToken({\n tokenStore: deps.tokenStore,\n pepper: deps.pepper,\n env,\n scopes: parsed.data.scopes,\n ...(parsed.data.label !== undefined ? { label: parsed.data.label } : {}),\n ...(parsed.data.expiresInMs !== undefined ? { expiresInMs: parsed.data.expiresInMs } : {}),\n });\n // Reveal the raw token exactly once - at creation.\n const raw = await created.raw.use((value) => value);\n return c.json(\n {\n token: {\n id: created.record.id,\n label: created.record.label,\n scopes: created.record.scopes,\n createdAt: created.record.createdAt,\n expiresAt: created.record.expiresAt,\n },\n raw,\n warning: 'Store this raw value securely; it is shown exactly once.',\n },\n 201,\n );\n });\n\n app.delete('/:id', createScopeMiddleware('tokens:revoke'), async (c) => {\n const id = c.req.param('id');\n const updated = await revokeToken(deps.tokenStore, id);\n if (updated === undefined) {\n return c.json({ error: 'token-not-found', message: `Token '${id}' not found.` }, 404);\n }\n return c.body(null, 204);\n });\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":";;;;;;AA4BA,MAAM,mBAAmB,EACtB,OAAO;CACN,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACnC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;CACzC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACjC,aAAa,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU;CACpD,CAAC,CACD,QAAQ;;;;AAKX,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,IAAI,KAAK,sBAAsB,cAAc,EAAE,OAAO,MAAM;EAG9D,MAAM,aAFU,MAAM,WAAW,KAAK,WAAW,EAEvB,KAAK,OAAO;GACpC,IAAI,EAAE;GACN,OAAO,EAAE;GACT,QAAQ,EAAE;GACV,WAAW,EAAE;GACb,YAAY,EAAE;GACd,WAAW,EAAE;GACb,WAAW,EAAE;GACd,EAAE;AACH,SAAO,EAAE,KAAK,EAAE,QAAQ,WAAW,CAAC;GACpC;AAEF,KAAI,KAAK,KAAK,sBAAsB,gBAAgB,EAAE,OAAO,MAAM;EACjE,MAAM,SAAS,iBAAiB,UAAU,MAAM,gBAAgB,EAAE,CAAC;AACnE,MAAI,CAAC,OAAO,QACV,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACT,QAAQ,OAAO,MAAM,OAAO,KAAK,OAAO;IAAE,MAAM,EAAE;IAAM,SAAS,EAAE;IAAS,EAAE;GAC/E,EACD,IACD;EAEH,MAAM,MAAM,OAAO,KAAK,OAAO,KAAK;AACpC,MAAI,CAAC,KAAK,YAAY,SAAS,IAAI,CACjC,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS,gBAAgB,IAAI;GAC7B,SAAS,KAAK;GACf,EACD,IACD;EAEH,MAAM,UAAU,MAAM,YAAY;GAChC,YAAY,KAAK;GACjB,QAAQ,KAAK;GACb;GACA,QAAQ,OAAO,KAAK;GACpB,GAAI,OAAO,KAAK,UAAU,SAAY,EAAE,OAAO,OAAO,KAAK,OAAO,GAAG,EAAE;GACvE,GAAI,OAAO,KAAK,gBAAgB,SAAY,EAAE,aAAa,OAAO,KAAK,aAAa,GAAG,EAAE;GAC1F,CAAC;EAEF,MAAM,MAAM,MAAM,QAAQ,IAAI,KAAK,UAAU,MAAM;AACnD,SAAO,EAAE,KACP;GACE,OAAO;IACL,IAAI,QAAQ,OAAO;IACnB,OAAO,QAAQ,OAAO;IACtB,QAAQ,QAAQ,OAAO;IACvB,WAAW,QAAQ,OAAO;IAC1B,WAAW,QAAQ,OAAO;IAC3B;GACD;GACA,SAAS;GACV,EACD,IACD;GACD;AAEF,KAAI,OAAO,QAAQ,sBAAsB,gBAAgB,EAAE,OAAO,MAAM;EACtE,MAAM,KAAK,EAAE,IAAI,MAAM,KAAK;AAE5B,MADgB,MAAM,YAAY,KAAK,YAAY,GAAG,KACtC,OACd,QAAO,EAAE,KAAK;GAAE,OAAO;GAAmB,SAAS,UAAU,GAAG;GAAe,EAAE,IAAI;AAEvF,SAAO,EAAE,KAAK,MAAM,IAAI;GACxB;AAEF,QAAO;;AAGT,eAAe,gBAAgB,GAA8D;AAC3F,KAAI;AACF,SAAO,MAAM,EAAE,IAAI,MAAM;SACnB;AACN,SAAO,EAAE"}
@@ -88,19 +88,21 @@ function createWorkflowRoutes(deps) {
88
88
  message: `Workflow '${id}' does not implement resume().`
89
89
  }, 400);
90
90
  const runId = newRunId();
91
- deps.runs.declare(runId, {
91
+ const tracker = deps.runs.start(runId, {
92
92
  kind: "workflow",
93
93
  workflowId: id,
94
94
  threadId: parsed.data.threadId
95
95
  });
96
+ const subject = `workflow:${id}/runs/${runId}/events`;
97
+ backgroundResume(workflow, parsed.data, tracker, deps.runs, runId, {
98
+ subject,
99
+ ...deps.dispatcher !== void 0 ? { dispatcher: deps.dispatcher } : {}
100
+ });
96
101
  return c.json({
97
102
  runId,
98
103
  threadId: parsed.data.threadId,
99
- status: "pending",
100
- subscribe: {
101
- websocket: `workflow:${id}/runs/${runId}/events`,
102
- sse: `/v1/runs/${runId}/events`
103
- }
104
+ status: "running",
105
+ subscribe: { websocket: subject }
104
106
  }, 202);
105
107
  });
106
108
  app.get("/:id/state", createScopeMiddleware((_path, params) => `workflows:read:${params.id}`), async (c) => {
@@ -174,10 +176,11 @@ function createWorkflowRoutes(deps) {
174
176
  }))
175
177
  }, 400);
176
178
  return c.json({
179
+ error: "workflow-fork-not-implemented",
180
+ message: "Workflow fork is not implemented on the server surface yet. Fork the thread programmatically via the workflow API.",
177
181
  workflowId: id,
178
- status: "pending",
179
182
  fork: parsed.data
180
- }, 202);
183
+ }, 501);
181
184
  });
182
185
  return app;
183
186
  }
@@ -207,6 +210,37 @@ function backgroundExecute(workflow, body, tracker, runs, runId, streaming) {
207
210
  }
208
211
  })();
209
212
  }
213
+ /**
214
+ * periphery-01: mirror of {@link backgroundExecute} for the resume
215
+ * path - iterate `workflow.resume(threadId, {resume})`, emit every
216
+ * event on the run subject, and complete the tracked run.
217
+ */
218
+ function backgroundResume(workflow, body, tracker, runs, runId, streaming) {
219
+ const resume = workflow.resume;
220
+ if (resume === void 0) return;
221
+ (async () => {
222
+ try {
223
+ for await (const ev of resume.call(workflow, body.threadId, body.resume !== void 0 ? { resume: body.resume } : {})) {
224
+ if (tracker.signal.aborted) break;
225
+ const type = typeof ev.type === "string" ? ev.type : "workflow.event";
226
+ streaming.dispatcher?.emit(streaming.subject, {
227
+ type,
228
+ payload: ev
229
+ });
230
+ }
231
+ runs.complete(runId, tracker.signal.aborted ? "aborted" : "completed");
232
+ } catch (err) {
233
+ streaming.dispatcher?.emit(streaming.subject, {
234
+ type: "workflow.error",
235
+ payload: {
236
+ runId,
237
+ message: err instanceof Error ? err.message : String(err)
238
+ }
239
+ });
240
+ runs.complete(runId, tracker.signal.aborted ? "aborted" : "failed", err);
241
+ }
242
+ })();
243
+ }
210
244
  async function safelyParseJson(c) {
211
245
  try {
212
246
  return await c.req.json();
@@ -1 +1 @@
1
- {"version":3,"file":"workflows.js","names":["opts: { signal?: AbortSignal; threadId?: string }"],"sources":["../../src/routes/workflows.ts"],"sourcesContent":["/**\n * Workflow REST routes.\n *\n * POST /workflows/:id/execute (idempotent; scope `workflows:execute`)\n * POST /workflows/:id/resume (scope `workflows:resume[:id]`)\n * GET /workflows/:id/state (scope `workflows:read`)\n * GET /workflows/:id/checkpoints (scope `workflows:read`)\n * POST /workflows/:id/fork (scope `workflows:execute`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport { WorkflowNotFoundError } from '../errors/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WorkflowRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface WorkflowRoutesDeps {\n readonly workflows: WorkflowRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /** Streaming dispatcher (IP-2): workflow events reach the run subject. */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst ExecuteBodySchema = z\n .object({\n input: z.unknown().optional(),\n threadId: z.string().min(1).optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\nconst ResumeBodySchema = z\n .object({\n threadId: z.string().min(1),\n resume: z.unknown().optional(),\n })\n .strict();\n\nconst ForkBodySchema = z\n .object({\n fromThreadId: z.string().min(1),\n fromCheckpointId: z.string().min(1).optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport function createWorkflowRoutes(\n deps: WorkflowRoutesDeps,\n): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('workflows:read'), (c) =>\n c.json({ workflows: deps.workflows.list() }),\n );\n\n app.post(\n '/:id/execute',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ExecuteBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid execute body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const runId = newRunId();\n const tracker = deps.runs.start(runId, {\n kind: 'workflow',\n workflowId: id,\n ...(parsed.data.threadId !== undefined ? { threadId: parsed.data.threadId } : {}),\n ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),\n ...(parsed.data.userId !== undefined ? { userId: parsed.data.userId } : {}),\n });\n // Workflows are streaming-first; the REST endpoint kicks off the\n // run in the background and returns 202 + runId. The actual\n // event stream is consumed via WebSocket / SSE in Phase 14b.\n const subject = `workflow:${id}/runs/${runId}/events`;\n backgroundExecute(workflow, parsed.data, tracker, deps.runs, runId, {\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // IP-2: the previously-advertised SSE URL was never\n // mounted; the WS subject (now in the grammar) delivers.\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n app.post(\n '/:id/resume',\n createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ResumeBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid resume body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n if (workflow.resume === undefined) {\n return c.json(\n {\n error: 'workflow-resume-unsupported',\n message: `Workflow '${id}' does not implement resume().`,\n },\n 400,\n );\n }\n const runId = newRunId();\n deps.runs.declare(runId, {\n kind: 'workflow',\n workflowId: id,\n threadId: parsed.data.threadId,\n });\n return c.json(\n {\n runId,\n threadId: parsed.data.threadId,\n status: 'pending',\n subscribe: {\n websocket: `workflow:${id}/runs/${runId}/events`,\n sse: `/v1/runs/${runId}/events`,\n },\n },\n 202,\n );\n },\n );\n\n app.get(\n '/:id/state',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.getState === undefined) {\n return c.json(\n {\n error: 'workflow-state-unsupported',\n message: 'Workflow does not implement getState().',\n },\n 400,\n );\n }\n const state = await workflow.getState(threadId);\n return c.json({ workflowId: id, threadId, state });\n },\n );\n\n app.get(\n '/:id/checkpoints',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.listCheckpoints === undefined) {\n return c.json(\n {\n error: 'workflow-checkpoints-unsupported',\n message: 'Workflow does not implement listCheckpoints().',\n },\n 400,\n );\n }\n const checkpoints = await workflow.listCheckpoints(threadId);\n return c.json({ workflowId: id, threadId, checkpoints });\n },\n );\n\n app.post(\n '/:id/fork',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ForkBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid fork body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n // Fork delegates to the workflow's own primitives in Phase 14b/c.\n // Phase 14a returns a structured 202 with the fork descriptor so\n // the API contract is reachable today.\n return c.json(\n {\n workflowId: id,\n status: 'pending',\n fork: parsed.data,\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\nfunction backgroundExecute(\n workflow: NonNullable<ReturnType<WorkflowRegistry['get']>>,\n body: z.infer<typeof ExecuteBodySchema>,\n tracker: { readonly signal: AbortSignal },\n runs: RunStateTracker,\n runId: string,\n streaming: {\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const opts: { signal?: AbortSignal; threadId?: string } = { signal: tracker.signal };\n if (body.threadId !== undefined) opts.threadId = body.threadId;\n void (async () => {\n try {\n // IP-2: every workflow event reaches the dispatcher — the old\n // loop iterated the stream and threw each event away.\n for await (const ev of workflow.execute(body.input ?? {}, opts)) {\n if (tracker.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'workflow.event';\n streaming.dispatcher?.emit(streaming.subject, { type, payload: ev });\n }\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, message: err instanceof Error ? err.message : String(err) },\n });\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workflows.js","names":["opts: { signal?: AbortSignal; threadId?: string }"],"sources":["../../src/routes/workflows.ts"],"sourcesContent":["/**\n * Workflow REST routes.\n *\n * POST /workflows/:id/execute (idempotent; scope `workflows:execute`)\n * POST /workflows/:id/resume (scope `workflows:resume[:id]`)\n * GET /workflows/:id/state (scope `workflows:read`)\n * GET /workflows/:id/checkpoints (scope `workflows:read`)\n * POST /workflows/:id/fork (scope `workflows:execute`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport { WorkflowNotFoundError } from '../errors/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WorkflowRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface WorkflowRoutesDeps {\n readonly workflows: WorkflowRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /** Streaming dispatcher (IP-2): workflow events reach the run subject. */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst ExecuteBodySchema = z\n .object({\n input: z.unknown().optional(),\n threadId: z.string().min(1).optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\nconst ResumeBodySchema = z\n .object({\n threadId: z.string().min(1),\n resume: z.unknown().optional(),\n })\n .strict();\n\nconst ForkBodySchema = z\n .object({\n fromThreadId: z.string().min(1),\n fromCheckpointId: z.string().min(1).optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport function createWorkflowRoutes(\n deps: WorkflowRoutesDeps,\n): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('workflows:read'), (c) =>\n c.json({ workflows: deps.workflows.list() }),\n );\n\n app.post(\n '/:id/execute',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ExecuteBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid execute body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const runId = newRunId();\n const tracker = deps.runs.start(runId, {\n kind: 'workflow',\n workflowId: id,\n ...(parsed.data.threadId !== undefined ? { threadId: parsed.data.threadId } : {}),\n ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),\n ...(parsed.data.userId !== undefined ? { userId: parsed.data.userId } : {}),\n });\n // Workflows are streaming-first; the REST endpoint kicks off the\n // run in the background and returns 202 + runId. The actual\n // event stream is consumed via WebSocket / SSE in Phase 14b.\n const subject = `workflow:${id}/runs/${runId}/events`;\n backgroundExecute(workflow, parsed.data, tracker, deps.runs, runId, {\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // IP-2: the previously-advertised SSE URL was never\n // mounted; the WS subject (now in the grammar) delivers.\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n app.post(\n '/:id/resume',\n createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ResumeBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid resume body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n if (workflow.resume === undefined) {\n return c.json(\n {\n error: 'workflow-resume-unsupported',\n message: `Workflow '${id}' does not implement resume().`,\n },\n 400,\n );\n }\n const runId = newRunId();\n // periphery-01: the old handler declared the run and returned 202\n // WITHOUT ever calling `workflow.resume(...)` - the run sat\n // `pending` forever and the advertised SSE URL was never mounted.\n // Resume now mirrors execute: background-iterate + emit on the WS\n // subject; the tracker completes the run.\n const tracker = deps.runs.start(runId, {\n kind: 'workflow',\n workflowId: id,\n threadId: parsed.data.threadId,\n });\n const subject = `workflow:${id}/runs/${runId}/events`;\n backgroundResume(workflow, parsed.data, tracker, deps.runs, runId, {\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n return c.json(\n {\n runId,\n threadId: parsed.data.threadId,\n status: 'running',\n subscribe: {\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n app.get(\n '/:id/state',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.getState === undefined) {\n return c.json(\n {\n error: 'workflow-state-unsupported',\n message: 'Workflow does not implement getState().',\n },\n 400,\n );\n }\n const state = await workflow.getState(threadId);\n return c.json({ workflowId: id, threadId, state });\n },\n );\n\n app.get(\n '/:id/checkpoints',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.listCheckpoints === undefined) {\n return c.json(\n {\n error: 'workflow-checkpoints-unsupported',\n message: 'Workflow does not implement listCheckpoints().',\n },\n 400,\n );\n }\n const checkpoints = await workflow.listCheckpoints(threadId);\n return c.json({ workflowId: id, threadId, checkpoints });\n },\n );\n\n app.post(\n '/:id/fork',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ForkBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid fork body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n // periphery-01: the old handler returned a 202 that forked\n // nothing - the same class of lie IP-14 removed from the agent\n // resume route. Honest 501 until the workflow fork primitive is\n // threaded through the registry.\n return c.json(\n {\n error: 'workflow-fork-not-implemented',\n message:\n 'Workflow fork is not implemented on the server surface yet. ' +\n 'Fork the thread programmatically via the workflow API.',\n workflowId: id,\n fork: parsed.data,\n },\n 501,\n );\n },\n );\n\n return app;\n}\n\nfunction backgroundExecute(\n workflow: NonNullable<ReturnType<WorkflowRegistry['get']>>,\n body: z.infer<typeof ExecuteBodySchema>,\n tracker: { readonly signal: AbortSignal },\n runs: RunStateTracker,\n runId: string,\n streaming: {\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const opts: { signal?: AbortSignal; threadId?: string } = { signal: tracker.signal };\n if (body.threadId !== undefined) opts.threadId = body.threadId;\n void (async () => {\n try {\n // IP-2: every workflow event reaches the dispatcher - the old\n // loop iterated the stream and threw each event away.\n for await (const ev of workflow.execute(body.input ?? {}, opts)) {\n if (tracker.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'workflow.event';\n streaming.dispatcher?.emit(streaming.subject, { type, payload: ev });\n }\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, message: err instanceof Error ? err.message : String(err) },\n });\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * periphery-01: mirror of {@link backgroundExecute} for the resume\n * path - iterate `workflow.resume(threadId, {resume})`, emit every\n * event on the run subject, and complete the tracked run.\n */\nfunction backgroundResume(\n workflow: NonNullable<ReturnType<WorkflowRegistry['get']>>,\n body: z.infer<typeof ResumeBodySchema>,\n tracker: { readonly signal: AbortSignal },\n runs: RunStateTracker,\n runId: string,\n streaming: {\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const resume = workflow.resume;\n if (resume === undefined) return;\n void (async () => {\n try {\n for await (const ev of resume.call(\n workflow,\n body.threadId,\n body.resume !== undefined ? { resume: body.resume } : {},\n )) {\n if (tracker.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'workflow.event';\n streaming.dispatcher?.emit(streaming.subject, { type, payload: ev });\n }\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, message: err instanceof Error ? err.message : String(err) },\n });\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
@@ -114,7 +114,7 @@ declare class RunStateTracker {
114
114
  /**
115
115
  * IP-15: invoked exactly once per run, the first time it settles into a
116
116
  * terminal state. Used to drive the run-count + duration metrics. Never
117
- * throws into the tracker wrap your handler if it might.
117
+ * throws into the tracker - wrap your handler if it might.
118
118
  */
119
119
  readonly onTerminal?: (info: TerminalRunInfo) => void;
120
120
  });
@@ -131,13 +131,13 @@ declare class RunStateTracker {
131
131
  /**
132
132
  * Number of runs currently in `pending` or `running`. Useful for
133
133
  * snapshots / metrics. Note that `pending` runs hold a reservation
134
- * but have not yet started any work see {@link runningCount} for
134
+ * but have not yet started any work - see {@link runningCount} for
135
135
  * the drain-blocking subset.
136
136
  */
137
137
  inflightCount(): number;
138
138
  /**
139
139
  * Number of runs with active work in progress (`running`). The
140
- * lifecycle drain blocks on this counter only pending runs are a
140
+ * lifecycle drain blocks on this counter only - pending runs are a
141
141
  * pure reservation (e.g. an awaited WS subscription) and can be
142
142
  * aborted immediately when SIGTERM arrives.
143
143
  */
@@ -105,7 +105,7 @@ var RunStateTracker = class {
105
105
  /**
106
106
  * Number of runs currently in `pending` or `running`. Useful for
107
107
  * snapshots / metrics. Note that `pending` runs hold a reservation
108
- * but have not yet started any work see {@link runningCount} for
108
+ * but have not yet started any work - see {@link runningCount} for
109
109
  * the drain-blocking subset.
110
110
  */
111
111
  inflightCount() {
@@ -115,7 +115,7 @@ var RunStateTracker = class {
115
115
  }
116
116
  /**
117
117
  * Number of runs with active work in progress (`running`). The
118
- * lifecycle drain blocks on this counter only pending runs are a
118
+ * lifecycle drain blocks on this counter only - pending runs are a
119
119
  * pure reservation (e.g. an awaited WS subscription) and can be
120
120
  * aborted immediately when SIGTERM arrives.
121
121
  */
@@ -163,7 +163,7 @@ const DEFAULT_RUN_RETENTION_MS = 5 * 6e4;
163
163
  /**
164
164
  * IP-16: schedule a periodic prune of terminal run records. Without this the
165
165
  * tracker's `prune()` was never called, so every run / stream / workflow left
166
- * a `RunRecord` (each holding an `AbortController`) in memory forever an
166
+ * a `RunRecord` (each holding an `AbortController`) in memory forever - an
167
167
  * unbounded leak on a long-living server. Returns a stop function that clears
168
168
  * the timer; the timer is `unref`-ed so it never keeps the process alive.
169
169
  */
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.js","names":["#records","#now","#onTerminal","record: RunRecord","#emitTerminal","base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] }"],"sources":["../../src/runtime/run-state.ts"],"sourcesContent":["/**\n * In-memory bookkeeping for in-flight agent / workflow runs. Exposes\n * a tiny CRUD surface route handlers consume to honour the\n * `GET /runs/:runId/state` and `POST /runs/:runId/abort` endpoints\n * declared in the runtime architecture.\n *\n * The full durable resume / replay path lives in Phase 14b/c on top\n * of the WebSocket layer + the consolidator daemon. Phase 14a only\n * needs enough state to:\n * - mint a runId on every `POST /run` / `POST /stream`,\n * - track its lifecycle (`pending` → `running` → `completed` / `failed` / `aborted`),\n * - propagate `AbortController.signal` so handlers can cancel.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable status discriminator for a run snapshot. Mirrors the values\n * exposed on the public REST surface.\n *\n * @stable\n */\nexport type RunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'aborted';\n\n/**\n * Identifying tag for the underlying execution kind. Workflows run\n * on the durable engine in `@graphorin/workflow`; agents run on the\n * `@graphorin/agent` runtime.\n *\n * @stable\n */\nexport type RunKind = 'agent' | 'workflow';\n\n/**\n * Terminal status a run can settle into (never `pending` / `running`).\n *\n * @stable\n */\nexport type TerminalRunStatus = Extract<RunStatus, 'completed' | 'failed' | 'aborted'>;\n\n/**\n * IP-15: payload handed to the {@link RunStateTracker} `onTerminal` callback\n * the first time a run reaches a terminal state. The server turns this into\n * the `graphorin_agent_runs_total` counter + `graphorin_agent_run_duration_seconds`\n * summary. `durationMs` is omitted for a run that was aborted before it ever\n * started (no meaningful wall-clock).\n *\n * @stable\n */\nexport interface TerminalRunInfo {\n readonly status: TerminalRunStatus;\n readonly kind: RunKind;\n readonly durationMs?: number;\n}\n\nfunction isTerminalStatus(status: RunStatus): status is TerminalRunStatus {\n return status === 'completed' || status === 'failed' || status === 'aborted';\n}\n\n/**\n * Snapshot returned by {@link RunStateTracker.snapshot}.\n *\n * @stable\n */\nexport interface RunStateSnapshot {\n readonly runId: string;\n readonly kind: RunKind;\n readonly status: RunStatus;\n readonly startedAt?: number;\n readonly completedAt?: number;\n readonly error?: { readonly message: string };\n readonly agentId?: string;\n readonly workflowId?: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n}\n\n/**\n * Bookkeeping descriptor recorded at run start. Either an agent run\n * (with `agentId`) or a workflow run (with `workflowId` + optional\n * `threadId`).\n *\n * @stable\n */\nexport type RunDescriptor =\n | {\n readonly kind: 'agent';\n readonly agentId: string;\n readonly sessionId?: string;\n readonly userId?: string;\n }\n | {\n readonly kind: 'workflow';\n readonly workflowId: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n };\n\n/**\n * In-flight handle returned by {@link RunStateTracker.start}. Handlers\n * pass `signal` into the underlying `agent.run / workflow.execute`\n * invocation so cancellation propagates instantly.\n *\n * @stable\n */\nexport interface RunHandle {\n readonly runId: string;\n readonly signal: AbortSignal;\n cancel(reason?: unknown): void;\n}\n\ninterface RunRecord {\n readonly runId: string;\n readonly kind: RunKind;\n readonly descriptor: RunDescriptor;\n status: RunStatus;\n controller: AbortController;\n startedAt?: number;\n completedAt?: number;\n error?: { readonly message: string };\n}\n\n/**\n * Pluggable tracker. The default in-memory implementation is the only\n * one shipped in Phase 14a; future phases plug in a SQLite-backed\n * variant so durable resume survives process restarts.\n *\n * @stable\n */\nexport class RunStateTracker {\n readonly #records: Map<string, RunRecord> = new Map();\n readonly #now: () => number;\n readonly #onTerminal: ((info: TerminalRunInfo) => void) | undefined;\n\n constructor(\n options: {\n readonly now?: () => number;\n /**\n * IP-15: invoked exactly once per run, the first time it settles into a\n * terminal state. Used to drive the run-count + duration metrics. Never\n * throws into the tracker — wrap your handler if it might.\n */\n readonly onTerminal?: (info: TerminalRunInfo) => void;\n } = {},\n ) {\n this.#now = options.now ?? Date.now;\n this.#onTerminal = options.onTerminal;\n }\n\n #emitTerminal(record: RunRecord): void {\n if (this.#onTerminal === undefined) return;\n if (!isTerminalStatus(record.status)) return;\n const durationMs =\n record.startedAt !== undefined && record.completedAt !== undefined\n ? record.completedAt - record.startedAt\n : undefined;\n this.#onTerminal({\n status: record.status,\n kind: record.kind,\n ...(durationMs !== undefined ? { durationMs } : {}),\n });\n }\n\n /** Reserve a run id without taking ownership of an AbortSignal. */\n declare(runId: string, descriptor: RunDescriptor): void {\n if (this.#records.has(runId)) return;\n const controller = new AbortController();\n this.#records.set(runId, {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller,\n });\n }\n\n /** Promote a previously-declared run to `running` (or declare it). */\n start(runId: string, descriptor: RunDescriptor): RunHandle {\n const existing = this.#records.get(runId);\n const record: RunRecord = existing ?? {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller: new AbortController(),\n };\n record.status = 'running';\n record.startedAt = this.#now();\n this.#records.set(runId, record);\n return Object.freeze({\n runId,\n signal: record.controller.signal,\n cancel: (reason?: unknown) => {\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n },\n });\n }\n\n /** Mark a run as terminal. */\n complete(\n runId: string,\n status: Extract<RunStatus, 'completed' | 'failed' | 'aborted'>,\n err?: unknown,\n ): void {\n const record = this.#records.get(runId);\n if (record === undefined) return;\n // IP-15: only the FIRST terminal transition counts toward the metrics —\n // a run aborted then re-completed must not double-increment.\n const wasTerminal = isTerminalStatus(record.status);\n record.status = status;\n record.completedAt = this.#now();\n if (err !== undefined) {\n record.error = { message: err instanceof Error ? err.message : String(err) };\n }\n if (!wasTerminal) this.#emitTerminal(record);\n }\n\n /** Cancel a run via its `AbortController`. */\n abort(runId: string, reason?: unknown): boolean {\n const record = this.#records.get(runId);\n if (record === undefined) return false;\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n if (record.status === 'pending' || record.status === 'running') {\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n }\n return true;\n }\n\n /** Read-only snapshot, safe to JSON.stringify. */\n snapshot(runId: string): RunStateSnapshot | undefined {\n const record = this.#records.get(runId);\n if (record === undefined) return undefined;\n const base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] } = {\n runId: record.runId,\n kind: record.kind,\n status: record.status,\n };\n if (record.startedAt !== undefined) base.startedAt = record.startedAt;\n if (record.completedAt !== undefined) base.completedAt = record.completedAt;\n if (record.error !== undefined) base.error = record.error;\n if (record.descriptor.kind === 'agent') {\n base.agentId = record.descriptor.agentId;\n } else {\n base.workflowId = record.descriptor.workflowId;\n if (record.descriptor.threadId !== undefined) base.threadId = record.descriptor.threadId;\n }\n if (record.descriptor.sessionId !== undefined) base.sessionId = record.descriptor.sessionId;\n if (record.descriptor.userId !== undefined) base.userId = record.descriptor.userId;\n return Object.freeze(base as RunStateSnapshot);\n }\n\n /**\n * Number of runs currently in `pending` or `running`. Useful for\n * snapshots / metrics. Note that `pending` runs hold a reservation\n * but have not yet started any work — see {@link runningCount} for\n * the drain-blocking subset.\n */\n inflightCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending' || record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Number of runs with active work in progress (`running`). The\n * lifecycle drain blocks on this counter only — pending runs are a\n * pure reservation (e.g. an awaited WS subscription) and can be\n * aborted immediately when SIGTERM arrives.\n */\n runningCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Drop every reserved-but-not-yet-started run. Called by the\n * server lifecycle at the start of `stop()` so the drain only\n * waits for actual work in flight.\n */\n abortPending(reason?: unknown): number {\n let aborted = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending') {\n if (!record.controller.signal.aborted) record.controller.abort(reason);\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n aborted += 1;\n }\n }\n return aborted;\n }\n\n /** Drop terminal records older than `olderThan`. */\n prune(olderThan: number): number {\n let removed = 0;\n for (const [runId, record] of this.#records) {\n if (\n record.completedAt !== undefined &&\n record.completedAt <= olderThan &&\n record.status !== 'pending' &&\n record.status !== 'running'\n ) {\n this.#records.delete(runId);\n removed += 1;\n }\n }\n return removed;\n }\n\n /** Cancel every in-flight run. Used during graceful shutdown. */\n abortAll(reason?: unknown): number {\n let aborted = 0;\n for (const [runId] of this.#records) {\n if (this.abort(runId, reason)) aborted += 1;\n }\n return aborted;\n }\n}\n\n/** Default cadence for the terminal-record prune sweep (IP-16). */\nexport const DEFAULT_RUN_PRUNE_INTERVAL_MS = 60_000;\n/** Default retention for terminal run records before they are dropped. */\nexport const DEFAULT_RUN_RETENTION_MS = 5 * 60_000;\n\n/**\n * IP-16: schedule a periodic prune of terminal run records. Without this the\n * tracker's `prune()` was never called, so every run / stream / workflow left\n * a `RunRecord` (each holding an `AbortController`) in memory forever — an\n * unbounded leak on a long-living server. Returns a stop function that clears\n * the timer; the timer is `unref`-ed so it never keeps the process alive.\n */\nexport function scheduleRunPruning(\n runs: RunStateTracker,\n now: () => number,\n opts: { readonly intervalMs?: number; readonly retentionMs?: number } = {},\n): () => void {\n const intervalMs = opts.intervalMs ?? DEFAULT_RUN_PRUNE_INTERVAL_MS;\n const retentionMs = opts.retentionMs ?? DEFAULT_RUN_RETENTION_MS;\n const timer = setInterval(() => {\n runs.prune(now() - retentionMs);\n }, intervalMs);\n if (typeof (timer as { unref?: () => void }).unref === 'function') {\n (timer as { unref: () => void }).unref();\n }\n return () => clearInterval(timer);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"run-state.js","names":["#records","#now","#onTerminal","record: RunRecord","#emitTerminal","base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] }"],"sources":["../../src/runtime/run-state.ts"],"sourcesContent":["/**\n * In-memory bookkeeping for in-flight agent / workflow runs. Exposes\n * a tiny CRUD surface route handlers consume to honour the\n * `GET /runs/:runId/state` and `POST /runs/:runId/abort` endpoints\n * declared in the runtime architecture.\n *\n * The full durable resume / replay path lives in Phase 14b/c on top\n * of the WebSocket layer + the consolidator daemon. Phase 14a only\n * needs enough state to:\n * - mint a runId on every `POST /run` / `POST /stream`,\n * - track its lifecycle (`pending` → `running` → `completed` / `failed` / `aborted`),\n * - propagate `AbortController.signal` so handlers can cancel.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable status discriminator for a run snapshot. Mirrors the values\n * exposed on the public REST surface.\n *\n * @stable\n */\nexport type RunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'aborted';\n\n/**\n * Identifying tag for the underlying execution kind. Workflows run\n * on the durable engine in `@graphorin/workflow`; agents run on the\n * `@graphorin/agent` runtime.\n *\n * @stable\n */\nexport type RunKind = 'agent' | 'workflow';\n\n/**\n * Terminal status a run can settle into (never `pending` / `running`).\n *\n * @stable\n */\nexport type TerminalRunStatus = Extract<RunStatus, 'completed' | 'failed' | 'aborted'>;\n\n/**\n * IP-15: payload handed to the {@link RunStateTracker} `onTerminal` callback\n * the first time a run reaches a terminal state. The server turns this into\n * the `graphorin_agent_runs_total` counter + `graphorin_agent_run_duration_seconds`\n * summary. `durationMs` is omitted for a run that was aborted before it ever\n * started (no meaningful wall-clock).\n *\n * @stable\n */\nexport interface TerminalRunInfo {\n readonly status: TerminalRunStatus;\n readonly kind: RunKind;\n readonly durationMs?: number;\n}\n\nfunction isTerminalStatus(status: RunStatus): status is TerminalRunStatus {\n return status === 'completed' || status === 'failed' || status === 'aborted';\n}\n\n/**\n * Snapshot returned by {@link RunStateTracker.snapshot}.\n *\n * @stable\n */\nexport interface RunStateSnapshot {\n readonly runId: string;\n readonly kind: RunKind;\n readonly status: RunStatus;\n readonly startedAt?: number;\n readonly completedAt?: number;\n readonly error?: { readonly message: string };\n readonly agentId?: string;\n readonly workflowId?: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n}\n\n/**\n * Bookkeeping descriptor recorded at run start. Either an agent run\n * (with `agentId`) or a workflow run (with `workflowId` + optional\n * `threadId`).\n *\n * @stable\n */\nexport type RunDescriptor =\n | {\n readonly kind: 'agent';\n readonly agentId: string;\n readonly sessionId?: string;\n readonly userId?: string;\n }\n | {\n readonly kind: 'workflow';\n readonly workflowId: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n };\n\n/**\n * In-flight handle returned by {@link RunStateTracker.start}. Handlers\n * pass `signal` into the underlying `agent.run / workflow.execute`\n * invocation so cancellation propagates instantly.\n *\n * @stable\n */\nexport interface RunHandle {\n readonly runId: string;\n readonly signal: AbortSignal;\n cancel(reason?: unknown): void;\n}\n\ninterface RunRecord {\n readonly runId: string;\n readonly kind: RunKind;\n readonly descriptor: RunDescriptor;\n status: RunStatus;\n controller: AbortController;\n startedAt?: number;\n completedAt?: number;\n error?: { readonly message: string };\n}\n\n/**\n * Pluggable tracker. The default in-memory implementation is the only\n * one shipped in Phase 14a; future phases plug in a SQLite-backed\n * variant so durable resume survives process restarts.\n *\n * @stable\n */\nexport class RunStateTracker {\n readonly #records: Map<string, RunRecord> = new Map();\n readonly #now: () => number;\n readonly #onTerminal: ((info: TerminalRunInfo) => void) | undefined;\n\n constructor(\n options: {\n readonly now?: () => number;\n /**\n * IP-15: invoked exactly once per run, the first time it settles into a\n * terminal state. Used to drive the run-count + duration metrics. Never\n * throws into the tracker - wrap your handler if it might.\n */\n readonly onTerminal?: (info: TerminalRunInfo) => void;\n } = {},\n ) {\n this.#now = options.now ?? Date.now;\n this.#onTerminal = options.onTerminal;\n }\n\n #emitTerminal(record: RunRecord): void {\n if (this.#onTerminal === undefined) return;\n if (!isTerminalStatus(record.status)) return;\n const durationMs =\n record.startedAt !== undefined && record.completedAt !== undefined\n ? record.completedAt - record.startedAt\n : undefined;\n this.#onTerminal({\n status: record.status,\n kind: record.kind,\n ...(durationMs !== undefined ? { durationMs } : {}),\n });\n }\n\n /** Reserve a run id without taking ownership of an AbortSignal. */\n declare(runId: string, descriptor: RunDescriptor): void {\n if (this.#records.has(runId)) return;\n const controller = new AbortController();\n this.#records.set(runId, {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller,\n });\n }\n\n /** Promote a previously-declared run to `running` (or declare it). */\n start(runId: string, descriptor: RunDescriptor): RunHandle {\n const existing = this.#records.get(runId);\n const record: RunRecord = existing ?? {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller: new AbortController(),\n };\n record.status = 'running';\n record.startedAt = this.#now();\n this.#records.set(runId, record);\n return Object.freeze({\n runId,\n signal: record.controller.signal,\n cancel: (reason?: unknown) => {\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n },\n });\n }\n\n /** Mark a run as terminal. */\n complete(\n runId: string,\n status: Extract<RunStatus, 'completed' | 'failed' | 'aborted'>,\n err?: unknown,\n ): void {\n const record = this.#records.get(runId);\n if (record === undefined) return;\n // IP-15: only the FIRST terminal transition counts toward the metrics -\n // a run aborted then re-completed must not double-increment.\n const wasTerminal = isTerminalStatus(record.status);\n record.status = status;\n record.completedAt = this.#now();\n if (err !== undefined) {\n record.error = { message: err instanceof Error ? err.message : String(err) };\n }\n if (!wasTerminal) this.#emitTerminal(record);\n }\n\n /** Cancel a run via its `AbortController`. */\n abort(runId: string, reason?: unknown): boolean {\n const record = this.#records.get(runId);\n if (record === undefined) return false;\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n if (record.status === 'pending' || record.status === 'running') {\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n }\n return true;\n }\n\n /** Read-only snapshot, safe to JSON.stringify. */\n snapshot(runId: string): RunStateSnapshot | undefined {\n const record = this.#records.get(runId);\n if (record === undefined) return undefined;\n const base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] } = {\n runId: record.runId,\n kind: record.kind,\n status: record.status,\n };\n if (record.startedAt !== undefined) base.startedAt = record.startedAt;\n if (record.completedAt !== undefined) base.completedAt = record.completedAt;\n if (record.error !== undefined) base.error = record.error;\n if (record.descriptor.kind === 'agent') {\n base.agentId = record.descriptor.agentId;\n } else {\n base.workflowId = record.descriptor.workflowId;\n if (record.descriptor.threadId !== undefined) base.threadId = record.descriptor.threadId;\n }\n if (record.descriptor.sessionId !== undefined) base.sessionId = record.descriptor.sessionId;\n if (record.descriptor.userId !== undefined) base.userId = record.descriptor.userId;\n return Object.freeze(base as RunStateSnapshot);\n }\n\n /**\n * Number of runs currently in `pending` or `running`. Useful for\n * snapshots / metrics. Note that `pending` runs hold a reservation\n * but have not yet started any work - see {@link runningCount} for\n * the drain-blocking subset.\n */\n inflightCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending' || record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Number of runs with active work in progress (`running`). The\n * lifecycle drain blocks on this counter only - pending runs are a\n * pure reservation (e.g. an awaited WS subscription) and can be\n * aborted immediately when SIGTERM arrives.\n */\n runningCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Drop every reserved-but-not-yet-started run. Called by the\n * server lifecycle at the start of `stop()` so the drain only\n * waits for actual work in flight.\n */\n abortPending(reason?: unknown): number {\n let aborted = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending') {\n if (!record.controller.signal.aborted) record.controller.abort(reason);\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n aborted += 1;\n }\n }\n return aborted;\n }\n\n /** Drop terminal records older than `olderThan`. */\n prune(olderThan: number): number {\n let removed = 0;\n for (const [runId, record] of this.#records) {\n if (\n record.completedAt !== undefined &&\n record.completedAt <= olderThan &&\n record.status !== 'pending' &&\n record.status !== 'running'\n ) {\n this.#records.delete(runId);\n removed += 1;\n }\n }\n return removed;\n }\n\n /** Cancel every in-flight run. Used during graceful shutdown. */\n abortAll(reason?: unknown): number {\n let aborted = 0;\n for (const [runId] of this.#records) {\n if (this.abort(runId, reason)) aborted += 1;\n }\n return aborted;\n }\n}\n\n/** Default cadence for the terminal-record prune sweep (IP-16). */\nexport const DEFAULT_RUN_PRUNE_INTERVAL_MS = 60_000;\n/** Default retention for terminal run records before they are dropped. */\nexport const DEFAULT_RUN_RETENTION_MS = 5 * 60_000;\n\n/**\n * IP-16: schedule a periodic prune of terminal run records. Without this the\n * tracker's `prune()` was never called, so every run / stream / workflow left\n * a `RunRecord` (each holding an `AbortController`) in memory forever - an\n * unbounded leak on a long-living server. Returns a stop function that clears\n * the timer; the timer is `unref`-ed so it never keeps the process alive.\n */\nexport function scheduleRunPruning(\n runs: RunStateTracker,\n now: () => number,\n opts: { readonly intervalMs?: number; readonly retentionMs?: number } = {},\n): () => void {\n const intervalMs = opts.intervalMs ?? DEFAULT_RUN_PRUNE_INTERVAL_MS;\n const retentionMs = opts.retentionMs ?? DEFAULT_RUN_RETENTION_MS;\n const timer = setInterval(() => {\n runs.prune(now() - retentionMs);\n }, intervalMs);\n if (typeof (timer as { unref?: () => void }).unref === 'function') {\n (timer as { unref: () => void }).unref();\n }\n return () => clearInterval(timer);\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ import { stream } from "hono/streaming";
13
13
  * `ServerMessage` event frames; control-plane operations
14
14
  * (subscribe / unsubscribe / abort / resume) go through REST.
15
15
  *
16
- * Unlike the WS surface, the SSE responder is not multiplexed
16
+ * Unlike the WS surface, the SSE responder is not multiplexed -
17
17
  * one HTTP request per session subject. Reconnection is handled by
18
18
  * the standard EventSource `Last-Event-ID` mechanism: when the
19
19
  * client supplies the header on resume, the responder walks the
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":["sanitizer: DeliveryCommentarySanitizer","lastEventId: string | undefined","queue: ServerMessage[]","resumeNotify: (() => void) | undefined"],"sources":["../../src/sse/events.ts"],"sourcesContent":["/**\n * Server-Sent Events fallback for non-WebSocket-friendly clients.\n *\n * The endpoint is unidirectional: the server pushes\n * `ServerMessage` event frames; control-plane operations\n * (subscribe / unsubscribe / abort / resume) go through REST.\n *\n * Unlike the WS surface, the SSE responder is not multiplexed —\n * one HTTP request per session subject. Reconnection is handled by\n * the standard EventSource `Last-Event-ID` mechanism: when the\n * client supplies the header on resume, the responder walks the\n * dispatcher's replay buffer from that cursor.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n isLifecycleFrame,\n type ServerEventFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { Hono } from 'hono';\nimport { stream } from 'hono/streaming';\nimport type { StreamingApi } from 'hono/utils/stream';\n\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsDispatcher } from '../ws/dispatcher.js';\n\n/**\n * Stable shape consumed by {@link createSseRoutes}.\n *\n * @stable\n */\nexport interface SseRoutesDeps {\n /**\n * Cap on the per-connection SSE delivery queue (IP-9). A consumer\n * that stops reading past this many buffered frames is closed\n * instead of growing the queue without bound. Default 1000.\n */\n readonly perConnectionQueueLimit?: number;\n readonly dispatcher: WsDispatcher;\n readonly commentary?: DeliveryCommentaryConfig;\n /**\n * How long the SSE responder waits between heartbeat comments to\n * keep proxies / load balancers from closing the idle connection.\n * Default `15_000` ms.\n */\n readonly keepAliveMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * Build the SSE event-stream router. Mounts\n * `GET /sessions/:id/events` (the canonical path documented in the\n * runtime spec); operators that want a different path should mount\n * the router under a custom prefix.\n *\n * @stable\n */\nexport function createSseRoutes(deps: SseRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const sanitizer: DeliveryCommentarySanitizer =\n deps.dispatcher.sanitizer.applyToEvents.length > 0\n ? deps.dispatcher.sanitizer\n : createDeliveryCommentarySanitizer(deps.commentary);\n const keepAliveMs = deps.keepAliveMs ?? 15_000;\n\n const app = new Hono<{ Variables: ServerVariables }>();\n app.get(\n '/:id/events',\n createScopeMiddleware('sessions:read'),\n sseHandler(deps.dispatcher, sanitizer, keepAliveMs, deps.perConnectionQueueLimit ?? 1000),\n );\n return app;\n}\n\nfunction sseHandler(\n dispatcher: WsDispatcher,\n sanitizer: DeliveryCommentarySanitizer,\n keepAliveMs: number,\n perConnectionQueueLimit: number,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n return async (c: Context<{ Variables: ServerVariables }>) => {\n const sessionId = c.req.param('id');\n if (sessionId === undefined || sessionId.length === 0) {\n return c.json(\n { error: 'session-id-required', message: 'Path parameter :id is required.' },\n 400,\n );\n }\n const subject = `session:${sessionId}/events`;\n const sinceEventId = c.req.header('last-event-id') ?? c.req.header('Last-Event-ID');\n\n c.header('Content-Type', 'text/event-stream; charset=utf-8');\n c.header('Cache-Control', 'no-cache, no-transform');\n c.header('Connection', 'keep-alive');\n c.header('X-Accel-Buffering', 'no');\n\n return stream(c, async (writer) => {\n const replay = dispatcher.replayBuffer.replay(subject, sinceEventId);\n let lastEventId: string | undefined;\n for (const frame of replay.events) {\n const sanitized = sanitizer.sanitize({ ...frame, subscriptionId: 'sse' }, 'sse');\n await writeFrame(writer as StreamingApi, sanitized);\n lastEventId = sanitized.eventId;\n }\n if (replay.droppedCount > 0) {\n await writeReplayMarker(writer as StreamingApi, replay.droppedCount, lastEventId);\n }\n // Live tail — register a synthetic subscription so the\n // dispatcher fans new events into this connection. The\n // dispatcher already validates + sanitizes every frame; here we\n // just write the bytes onto the wire.\n const subscriberId = `sse-${Math.random().toString(36).slice(2, 10)}`;\n const subscriptionId = `${subscriberId}-sub`;\n const authState = c.get('state').auth;\n // IP-13: the anonymous (auth.kind='none') principal carries `admin:*` in\n // grantedScopes, so the dispatcher's per-subject scope check still passes.\n const grantedScopes =\n authState.kind === 'token' || authState.kind === 'anonymous' ? authState.grantedScopes : [];\n const tokenId =\n authState.kind === 'token'\n ? authState.token.tokenId\n : authState.kind === 'anonymous'\n ? 'anonymous'\n : 'sse-anon';\n let queue: ServerMessage[] = [];\n let closedByBackpressure = false;\n const queueLimit = perConnectionQueueLimit;\n let resumeNotify: (() => void) | undefined;\n const notify = (): void => {\n if (resumeNotify !== undefined) {\n const fn = resumeNotify;\n resumeNotify = undefined;\n fn();\n }\n };\n const handle = {\n id: subscriberId,\n tokenId,\n // The strict-mode subscribe path enforces scope; we trust the\n // outer middleware (`createScopeMiddleware('sessions:read')`)\n // to gate this handler so we forward an empty scope set —\n // the dispatcher's per-subject scope check still requires\n // the `agents:invoke:<sessionId>` grant.\n grantedScopes,\n send: (frame: ServerMessage) => {\n // IP-9: bound the queue — a stalled consumer is closed, not\n // buffered into the heap forever.\n if (queue.length >= queueLimit) {\n closedByBackpressure = true;\n queue = [];\n notify();\n return;\n }\n queue.push(frame);\n notify();\n },\n close: () => {\n queue = [];\n notify();\n },\n };\n const reg = dispatcher.registerSubscriber(handle);\n const result = dispatcher.subscribe({\n subscriberId,\n subject,\n subscriptionId,\n ...(lastEventId !== undefined ? { sinceEventId: lastEventId } : {}),\n });\n if (!result.ok) {\n reg.unregister();\n await writer.write(\n encodeSse({\n event: 'error',\n data: JSON.stringify({\n error: 'sse-subscribe-failed',\n reason: result.reason,\n }),\n }),\n );\n await writer.close();\n return;\n }\n const heartbeat = setInterval(() => {\n // Fire-and-forget heartbeat comment — clients ignore comment\n // lines (RFC 6455 § 9.1).\n writer.write(': keep-alive\\n\\n').catch(() => undefined);\n }, keepAliveMs);\n let aborted = false;\n const onAbort = (): void => {\n aborted = true;\n notify();\n };\n c.req.raw.signal.addEventListener('abort', onAbort, { once: true });\n try {\n while (!aborted) {\n if (closedByBackpressure) {\n // IP-9: the consumer stalled past the queue cap — close the\n // stream with a terminal lifecycle frame instead of\n // buffering unboundedly.\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify({\n kind: 'lifecycle',\n subscriptionId,\n status: 'failed',\n reason: 'flow.throttled',\n }),\n }),\n );\n aborted = true;\n continue;\n }\n if (queue.length > 0) {\n const next = queue.shift();\n if (next === undefined) continue;\n if (isEventFrame(next)) {\n await writeFrame(writer as StreamingApi, next);\n } else if (isLifecycleFrame(next)) {\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify(next),\n }),\n );\n if (\n next.status === 'completed' ||\n next.status === 'aborted' ||\n next.status === 'failed'\n ) {\n aborted = true;\n }\n }\n continue;\n }\n await new Promise<void>((resolve) => {\n resumeNotify = resolve;\n });\n }\n } finally {\n clearInterval(heartbeat);\n c.req.raw.signal.removeEventListener('abort', onAbort);\n dispatcher.unsubscribe(subscriptionId);\n reg.unregister();\n }\n });\n };\n}\n\nasync function writeFrame(writer: StreamingApi, frame: ServerEventFrame): Promise<void> {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) return;\n await writer.write(\n encodeSse({\n event: frame.type,\n id: frame.eventId,\n data: JSON.stringify(frame),\n }),\n );\n}\n\nasync function writeReplayMarker(\n writer: StreamingApi,\n droppedCount: number,\n lastEventId: string | undefined,\n): Promise<void> {\n await writer.write(\n encodeSse({\n ...(lastEventId !== undefined ? { id: lastEventId } : {}),\n event: 'replay-marker',\n data: JSON.stringify({\n v: '1',\n kind: 'replay-marker',\n subscriptionId: 'sse',\n eventId: lastEventId ?? `evt-replay-${Date.now().toString(36)}`,\n droppedCount,\n }),\n }),\n );\n}\n\nfunction encodeSse(frame: { event?: string; id?: string; data: string }): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}\\n`;\n if (frame.id !== undefined) out += `id: ${frame.id}\\n`;\n for (const line of frame.data.split('\\n')) {\n out += `data: ${line}\\n`;\n }\n out += '\\n';\n return out;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"events.js","names":["sanitizer: DeliveryCommentarySanitizer","lastEventId: string | undefined","queue: ServerMessage[]","resumeNotify: (() => void) | undefined"],"sources":["../../src/sse/events.ts"],"sourcesContent":["/**\n * Server-Sent Events fallback for non-WebSocket-friendly clients.\n *\n * The endpoint is unidirectional: the server pushes\n * `ServerMessage` event frames; control-plane operations\n * (subscribe / unsubscribe / abort / resume) go through REST.\n *\n * Unlike the WS surface, the SSE responder is not multiplexed -\n * one HTTP request per session subject. Reconnection is handled by\n * the standard EventSource `Last-Event-ID` mechanism: when the\n * client supplies the header on resume, the responder walks the\n * dispatcher's replay buffer from that cursor.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n isLifecycleFrame,\n type ServerEventFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { Hono } from 'hono';\nimport { stream } from 'hono/streaming';\nimport type { StreamingApi } from 'hono/utils/stream';\n\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsDispatcher } from '../ws/dispatcher.js';\n\n/**\n * Stable shape consumed by {@link createSseRoutes}.\n *\n * @stable\n */\nexport interface SseRoutesDeps {\n /**\n * Cap on the per-connection SSE delivery queue (IP-9). A consumer\n * that stops reading past this many buffered frames is closed\n * instead of growing the queue without bound. Default 1000.\n */\n readonly perConnectionQueueLimit?: number;\n readonly dispatcher: WsDispatcher;\n readonly commentary?: DeliveryCommentaryConfig;\n /**\n * How long the SSE responder waits between heartbeat comments to\n * keep proxies / load balancers from closing the idle connection.\n * Default `15_000` ms.\n */\n readonly keepAliveMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * Build the SSE event-stream router. Mounts\n * `GET /sessions/:id/events` (the canonical path documented in the\n * runtime spec); operators that want a different path should mount\n * the router under a custom prefix.\n *\n * @stable\n */\nexport function createSseRoutes(deps: SseRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const sanitizer: DeliveryCommentarySanitizer =\n deps.dispatcher.sanitizer.applyToEvents.length > 0\n ? deps.dispatcher.sanitizer\n : createDeliveryCommentarySanitizer(deps.commentary);\n const keepAliveMs = deps.keepAliveMs ?? 15_000;\n\n const app = new Hono<{ Variables: ServerVariables }>();\n app.get(\n '/:id/events',\n createScopeMiddleware('sessions:read'),\n sseHandler(deps.dispatcher, sanitizer, keepAliveMs, deps.perConnectionQueueLimit ?? 1000),\n );\n return app;\n}\n\nfunction sseHandler(\n dispatcher: WsDispatcher,\n sanitizer: DeliveryCommentarySanitizer,\n keepAliveMs: number,\n perConnectionQueueLimit: number,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n return async (c: Context<{ Variables: ServerVariables }>) => {\n const sessionId = c.req.param('id');\n if (sessionId === undefined || sessionId.length === 0) {\n return c.json(\n { error: 'session-id-required', message: 'Path parameter :id is required.' },\n 400,\n );\n }\n const subject = `session:${sessionId}/events`;\n const sinceEventId = c.req.header('last-event-id') ?? c.req.header('Last-Event-ID');\n\n c.header('Content-Type', 'text/event-stream; charset=utf-8');\n c.header('Cache-Control', 'no-cache, no-transform');\n c.header('Connection', 'keep-alive');\n c.header('X-Accel-Buffering', 'no');\n\n return stream(c, async (writer) => {\n const replay = dispatcher.replayBuffer.replay(subject, sinceEventId);\n let lastEventId: string | undefined;\n for (const frame of replay.events) {\n const sanitized = sanitizer.sanitize({ ...frame, subscriptionId: 'sse' }, 'sse');\n await writeFrame(writer as StreamingApi, sanitized);\n lastEventId = sanitized.eventId;\n }\n if (replay.droppedCount > 0) {\n await writeReplayMarker(writer as StreamingApi, replay.droppedCount, lastEventId);\n }\n // Live tail - register a synthetic subscription so the\n // dispatcher fans new events into this connection. The\n // dispatcher already validates + sanitizes every frame; here we\n // just write the bytes onto the wire.\n const subscriberId = `sse-${Math.random().toString(36).slice(2, 10)}`;\n const subscriptionId = `${subscriberId}-sub`;\n const authState = c.get('state').auth;\n // IP-13: the anonymous (auth.kind='none') principal carries `admin:*` in\n // grantedScopes, so the dispatcher's per-subject scope check still passes.\n const grantedScopes =\n authState.kind === 'token' || authState.kind === 'anonymous' ? authState.grantedScopes : [];\n const tokenId =\n authState.kind === 'token'\n ? authState.token.tokenId\n : authState.kind === 'anonymous'\n ? 'anonymous'\n : 'sse-anon';\n let queue: ServerMessage[] = [];\n let closedByBackpressure = false;\n const queueLimit = perConnectionQueueLimit;\n let resumeNotify: (() => void) | undefined;\n const notify = (): void => {\n if (resumeNotify !== undefined) {\n const fn = resumeNotify;\n resumeNotify = undefined;\n fn();\n }\n };\n const handle = {\n id: subscriberId,\n tokenId,\n // The strict-mode subscribe path enforces scope; we trust the\n // outer middleware (`createScopeMiddleware('sessions:read')`)\n // to gate this handler so we forward an empty scope set -\n // the dispatcher's per-subject scope check still requires\n // the `agents:invoke:<sessionId>` grant.\n grantedScopes,\n send: (frame: ServerMessage) => {\n // IP-9: bound the queue - a stalled consumer is closed, not\n // buffered into the heap forever.\n if (queue.length >= queueLimit) {\n closedByBackpressure = true;\n queue = [];\n notify();\n return;\n }\n queue.push(frame);\n notify();\n },\n close: () => {\n queue = [];\n notify();\n },\n };\n const reg = dispatcher.registerSubscriber(handle);\n const result = dispatcher.subscribe({\n subscriberId,\n subject,\n subscriptionId,\n ...(lastEventId !== undefined ? { sinceEventId: lastEventId } : {}),\n });\n if (!result.ok) {\n reg.unregister();\n await writer.write(\n encodeSse({\n event: 'error',\n data: JSON.stringify({\n error: 'sse-subscribe-failed',\n reason: result.reason,\n }),\n }),\n );\n await writer.close();\n return;\n }\n const heartbeat = setInterval(() => {\n // Fire-and-forget heartbeat comment - clients ignore comment\n // lines (RFC 6455 § 9.1).\n writer.write(': keep-alive\\n\\n').catch(() => undefined);\n }, keepAliveMs);\n let aborted = false;\n const onAbort = (): void => {\n aborted = true;\n notify();\n };\n c.req.raw.signal.addEventListener('abort', onAbort, { once: true });\n try {\n while (!aborted) {\n if (closedByBackpressure) {\n // IP-9: the consumer stalled past the queue cap - close the\n // stream with a terminal lifecycle frame instead of\n // buffering unboundedly.\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify({\n kind: 'lifecycle',\n subscriptionId,\n status: 'failed',\n reason: 'flow.throttled',\n }),\n }),\n );\n aborted = true;\n continue;\n }\n if (queue.length > 0) {\n const next = queue.shift();\n if (next === undefined) continue;\n if (isEventFrame(next)) {\n await writeFrame(writer as StreamingApi, next);\n } else if (isLifecycleFrame(next)) {\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify(next),\n }),\n );\n if (\n next.status === 'completed' ||\n next.status === 'aborted' ||\n next.status === 'failed'\n ) {\n aborted = true;\n }\n }\n continue;\n }\n await new Promise<void>((resolve) => {\n resumeNotify = resolve;\n });\n }\n } finally {\n clearInterval(heartbeat);\n c.req.raw.signal.removeEventListener('abort', onAbort);\n dispatcher.unsubscribe(subscriptionId);\n reg.unregister();\n }\n });\n };\n}\n\nasync function writeFrame(writer: StreamingApi, frame: ServerEventFrame): Promise<void> {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) return;\n await writer.write(\n encodeSse({\n event: frame.type,\n id: frame.eventId,\n data: JSON.stringify(frame),\n }),\n );\n}\n\nasync function writeReplayMarker(\n writer: StreamingApi,\n droppedCount: number,\n lastEventId: string | undefined,\n): Promise<void> {\n await writer.write(\n encodeSse({\n ...(lastEventId !== undefined ? { id: lastEventId } : {}),\n event: 'replay-marker',\n data: JSON.stringify({\n v: '1',\n kind: 'replay-marker',\n subscriptionId: 'sse',\n eventId: lastEventId ?? `evt-replay-${Date.now().toString(36)}`,\n droppedCount,\n }),\n }),\n );\n}\n\nfunction encodeSse(frame: { event?: string; id?: string; data: string }): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}\\n`;\n if (frame.id !== undefined) out += `id: ${frame.id}\\n`;\n for (const line of frame.data.split('\\n')) {\n out += `data: ${line}\\n`;\n }\n out += '\\n';\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,SAAgB,gBAAgB,MAA2D;CACzF,MAAMA,YACJ,KAAK,WAAW,UAAU,cAAc,SAAS,IAC7C,KAAK,WAAW,YAChB,kCAAkC,KAAK,WAAW;CACxD,MAAM,cAAc,KAAK,eAAe;CAExC,MAAM,MAAM,IAAI,MAAsC;AACtD,KAAI,IACF,eACA,sBAAsB,gBAAgB,EACtC,WAAW,KAAK,YAAY,WAAW,aAAa,KAAK,2BAA2B,IAAK,CAC1F;AACD,QAAO;;AAGT,SAAS,WACP,YACA,WACA,aACA,yBACmD;AACnD,QAAO,OAAO,MAA+C;EAC3D,MAAM,YAAY,EAAE,IAAI,MAAM,KAAK;AACnC,MAAI,cAAc,UAAa,UAAU,WAAW,EAClD,QAAO,EAAE,KACP;GAAE,OAAO;GAAuB,SAAS;GAAmC,EAC5E,IACD;EAEH,MAAM,UAAU,WAAW,UAAU;EACrC,MAAM,eAAe,EAAE,IAAI,OAAO,gBAAgB,IAAI,EAAE,IAAI,OAAO,gBAAgB;AAEnF,IAAE,OAAO,gBAAgB,mCAAmC;AAC5D,IAAE,OAAO,iBAAiB,yBAAyB;AACnD,IAAE,OAAO,cAAc,aAAa;AACpC,IAAE,OAAO,qBAAqB,KAAK;AAEnC,SAAO,OAAO,GAAG,OAAO,WAAW;GACjC,MAAM,SAAS,WAAW,aAAa,OAAO,SAAS,aAAa;GACpE,IAAIC;AACJ,QAAK,MAAM,SAAS,OAAO,QAAQ;IACjC,MAAM,YAAY,UAAU,SAAS;KAAE,GAAG;KAAO,gBAAgB;KAAO,EAAE,MAAM;AAChF,UAAM,WAAW,QAAwB,UAAU;AACnD,kBAAc,UAAU;;AAE1B,OAAI,OAAO,eAAe,EACxB,OAAM,kBAAkB,QAAwB,OAAO,cAAc,YAAY;GAMnF,MAAM,eAAe,OAAO,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG;GACnE,MAAM,iBAAiB,GAAG,aAAa;GACvC,MAAM,YAAY,EAAE,IAAI,QAAQ,CAAC;GAGjC,MAAM,gBACJ,UAAU,SAAS,WAAW,UAAU,SAAS,cAAc,UAAU,gBAAgB,EAAE;GAC7F,MAAM,UACJ,UAAU,SAAS,UACf,UAAU,MAAM,UAChB,UAAU,SAAS,cACjB,cACA;GACR,IAAIC,QAAyB,EAAE;GAC/B,IAAI,uBAAuB;GAC3B,MAAM,aAAa;GACnB,IAAIC;GACJ,MAAM,eAAqB;AACzB,QAAI,iBAAiB,QAAW;KAC9B,MAAM,KAAK;AACX,oBAAe;AACf,SAAI;;;GAGR,MAAM,SAAS;IACb,IAAI;IACJ;IAMA;IACA,OAAO,UAAyB;AAG9B,SAAI,MAAM,UAAU,YAAY;AAC9B,6BAAuB;AACvB,cAAQ,EAAE;AACV,cAAQ;AACR;;AAEF,WAAM,KAAK,MAAM;AACjB,aAAQ;;IAEV,aAAa;AACX,aAAQ,EAAE;AACV,aAAQ;;IAEX;GACD,MAAM,MAAM,WAAW,mBAAmB,OAAO;GACjD,MAAM,SAAS,WAAW,UAAU;IAClC;IACA;IACA;IACA,GAAI,gBAAgB,SAAY,EAAE,cAAc,aAAa,GAAG,EAAE;IACnE,CAAC;AACF,OAAI,CAAC,OAAO,IAAI;AACd,QAAI,YAAY;AAChB,UAAM,OAAO,MACX,UAAU;KACR,OAAO;KACP,MAAM,KAAK,UAAU;MACnB,OAAO;MACP,QAAQ,OAAO;MAChB,CAAC;KACH,CAAC,CACH;AACD,UAAM,OAAO,OAAO;AACpB;;GAEF,MAAM,YAAY,kBAAkB;AAGlC,WAAO,MAAM,mBAAmB,CAAC,YAAY,OAAU;MACtD,YAAY;GACf,IAAI,UAAU;GACd,MAAM,gBAAsB;AAC1B,cAAU;AACV,YAAQ;;AAEV,KAAE,IAAI,IAAI,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;AACnE,OAAI;AACF,WAAO,CAAC,SAAS;AACf,SAAI,sBAAsB;AAIxB,YAAM,OAAO,MACX,UAAU;OACR,OAAO;OACP,MAAM,KAAK,UAAU;QACnB,MAAM;QACN;QACA,QAAQ;QACR,QAAQ;QACT,CAAC;OACH,CAAC,CACH;AACD,gBAAU;AACV;;AAEF,SAAI,MAAM,SAAS,GAAG;MACpB,MAAM,OAAO,MAAM,OAAO;AAC1B,UAAI,SAAS,OAAW;AACxB,UAAI,aAAa,KAAK,CACpB,OAAM,WAAW,QAAwB,KAAK;eACrC,iBAAiB,KAAK,EAAE;AACjC,aAAM,OAAO,MACX,UAAU;QACR,OAAO;QACP,MAAM,KAAK,UAAU,KAAK;QAC3B,CAAC,CACH;AACD,WACE,KAAK,WAAW,eAChB,KAAK,WAAW,aAChB,KAAK,WAAW,SAEhB,WAAU;;AAGd;;AAEF,WAAM,IAAI,SAAe,YAAY;AACnC,qBAAe;OACf;;aAEI;AACR,kBAAc,UAAU;AACxB,MAAE,IAAI,IAAI,OAAO,oBAAoB,SAAS,QAAQ;AACtD,eAAW,YAAY,eAAe;AACtC,QAAI,YAAY;;IAElB;;;AAIN,eAAe,WAAW,QAAsB,OAAwC;AAEtF,KAAI,CADc,oBAAoB,UAAU,MAAM,CACvC,QAAS;AACxB,OAAM,OAAO,MACX,UAAU;EACR,OAAO,MAAM;EACb,IAAI,MAAM;EACV,MAAM,KAAK,UAAU,MAAM;EAC5B,CAAC,CACH;;AAGH,eAAe,kBACb,QACA,cACA,aACe;AACf,OAAM,OAAO,MACX,UAAU;EACR,GAAI,gBAAgB,SAAY,EAAE,IAAI,aAAa,GAAG,EAAE;EACxD,OAAO;EACP,MAAM,KAAK,UAAU;GACnB,GAAG;GACH,MAAM;GACN,gBAAgB;GAChB,SAAS,eAAe,cAAc,KAAK,KAAK,CAAC,SAAS,GAAG;GAC7D;GACD,CAAC;EACH,CAAC,CACH;;AAGH,SAAS,UAAU,OAA8D;CAC/E,IAAI,MAAM;AACV,KAAI,MAAM,UAAU,OAAW,QAAO,UAAU,MAAM,MAAM;AAC5D,KAAI,MAAM,OAAO,OAAW,QAAO,OAAO,MAAM,GAAG;AACnD,MAAK,MAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,CACvC,QAAO,SAAS,KAAK;AAEvB,QAAO;AACP,QAAO"}