@graphorin/server 0.5.0 → 0.6.1

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 (79) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +9 -9
  3. package/dist/app.d.ts +5 -5
  4. package/dist/app.d.ts.map +1 -1
  5. package/dist/app.js +11 -10
  6. package/dist/app.js.map +1 -1
  7. package/dist/commentary/audit-bridge.d.ts +2 -2
  8. package/dist/commentary/audit-bridge.js +3 -3
  9. package/dist/commentary/audit-bridge.js.map +1 -1
  10. package/dist/commentary/built-in-patterns.js +1 -1
  11. package/dist/commentary/built-in-patterns.js.map +1 -1
  12. package/dist/commentary/sanitizer.js +2 -2
  13. package/dist/commentary/sanitizer.js.map +1 -1
  14. package/dist/commentary/types.d.ts +5 -5
  15. package/dist/consolidator/daemon.d.ts +1 -1
  16. package/dist/consolidator/daemon.js.map +1 -1
  17. package/dist/health/checks.js.map +1 -1
  18. package/dist/health/routes.d.ts +2 -2
  19. package/dist/health/routes.js.map +1 -1
  20. package/dist/index.d.ts +7 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal/ids.js +1 -1
  25. package/dist/internal/ids.js.map +1 -1
  26. package/dist/lifecycle/pre-bind.js.map +1 -1
  27. package/dist/metrics/catalog.js +1 -1
  28. package/dist/metrics/catalog.js.map +1 -1
  29. package/dist/metrics/registry.d.ts +2 -2
  30. package/dist/metrics/registry.js +1 -1
  31. package/dist/metrics/registry.js.map +1 -1
  32. package/dist/middleware/auth.d.ts +1 -1
  33. package/dist/middleware/auth.js +1 -1
  34. package/dist/middleware/auth.js.map +1 -1
  35. package/dist/middleware/cors.js.map +1 -1
  36. package/dist/middleware/csrf.js.map +1 -1
  37. package/dist/middleware/idempotency.d.ts +1 -1
  38. package/dist/middleware/idempotency.js +36 -23
  39. package/dist/middleware/idempotency.js.map +1 -1
  40. package/dist/middleware/rate-limit.js.map +1 -1
  41. package/dist/middleware/request-state.js.map +1 -1
  42. package/dist/middleware/scope.js.map +1 -1
  43. package/dist/package.js +6 -0
  44. package/dist/package.js.map +1 -0
  45. package/dist/registry/index.d.ts +2 -2
  46. package/dist/registry/index.js +1 -1
  47. package/dist/registry/index.js.map +1 -1
  48. package/dist/replay/routes.js +1 -1
  49. package/dist/replay/routes.js.map +1 -1
  50. package/dist/routes/agents.js +1 -1
  51. package/dist/routes/agents.js.map +1 -1
  52. package/dist/routes/audit.d.ts +1 -1
  53. package/dist/routes/audit.js.map +1 -1
  54. package/dist/routes/auth.js.map +1 -1
  55. package/dist/routes/health.js +1 -1
  56. package/dist/routes/health.js.map +1 -1
  57. package/dist/routes/sessions.js.map +1 -1
  58. package/dist/routes/tokens.js.map +1 -1
  59. package/dist/routes/workflows.js +42 -8
  60. package/dist/routes/workflows.js.map +1 -1
  61. package/dist/runtime/run-state.d.ts +3 -3
  62. package/dist/runtime/run-state.js +3 -3
  63. package/dist/runtime/run-state.js.map +1 -1
  64. package/dist/sse/events.js +1 -1
  65. package/dist/sse/events.js.map +1 -1
  66. package/dist/triggers/daemon.js.map +1 -1
  67. package/dist/triggers/routes.js.map +1 -1
  68. package/dist/ws/dispatcher.js.map +1 -1
  69. package/dist/ws/subjects.d.ts +1 -1
  70. package/dist/ws/subjects.d.ts.map +1 -1
  71. package/dist/ws/subjects.js +3 -3
  72. package/dist/ws/subjects.js.map +1 -1
  73. package/dist/ws/ticket.d.ts +1 -1
  74. package/dist/ws/ticket.js +1 -1
  75. package/dist/ws/ticket.js.map +1 -1
  76. package/dist/ws/upgrade.d.ts.map +1 -1
  77. package/dist/ws/upgrade.js +7 -5
  78. package/dist/ws/upgrade.js.map +1 -1
  79. package/package.json +16 -16
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":["SCOPE_SANITIZED: ParsedScope","SCOPE_RAW: ParsedScope","mode: ReplayMode"],"sources":["../../src/replay/routes.ts"],"sourcesContent":["/**\n * Replay endpoints with scope enforcement.\n *\n * POST /v1/runs/:runId/replay\n * POST /v1/sessions/:id/replay\n *\n * Scope rules:\n * - `raw: false` (default) → require `traces:read:sanitized`.\n * - `raw: true` → require `traces:read:raw` (admin).\n *\n * Every successful invocation appends a `trace.replay.accessed` audit\n * entry. The audit write is best-effort: a missing `AuditDb` does not\n * block the replay.\n *\n * The actual replay stream is consumer-supplied — the framework\n * accepts a `ReplayApi` from the operator that knows how to load\n * trace events for a `runId` / `sessionId`. Phase 14c only owns the\n * scope check + audit + transport.\n *\n * @packageDocumentation\n */\n\nimport { type AuditDb, appendAudit } from '@graphorin/security/audit';\nimport { type ParsedScope, parseScope, scopeMatches } from '@graphorin/security/auth';\nimport { Hono } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\nimport { z } from 'zod';\n\nimport type { AuthState, ServerVariables } from '../internal/context.js';\n\n/** @stable */\nexport type ReplayMode = 'sanitized' | 'raw';\n\n/** @stable */\nexport interface ReplayApi {\n loadRunReplay(input: {\n readonly runId: string;\n readonly mode: ReplayMode;\n readonly fromMessageId?: string;\n readonly provider?: string;\n }): Promise<ReplayResponse>;\n loadSessionReplay(input: {\n readonly sessionId: string;\n readonly mode: ReplayMode;\n readonly fromMessageId?: string;\n readonly provider?: string;\n }): Promise<ReplayResponse>;\n}\n\n/** @stable */\nexport interface ReplayResponse {\n readonly events: ReadonlyArray<unknown>;\n readonly truncated?: boolean;\n readonly nextCursor?: string;\n}\n\nconst SCOPE_SANITIZED: ParsedScope = parseScope('traces:read:sanitized');\nconst SCOPE_RAW: ParsedScope = parseScope('traces:read:raw');\n\nconst ReplayRequestSchema = z\n .object({\n raw: z.boolean().optional(),\n fromMessageId: z.string().min(1).optional(),\n provider: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface ReplayRoutesDeps {\n readonly replay: ReplayApi;\n readonly auditDb?: AuditDb;\n readonly now?: () => number;\n}\n\n/**\n * @stable\n */\nexport function createReplayRoutes(deps: ReplayRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const now = deps.now ?? Date.now;\n\n app.post('/runs/:runId/replay', async (c) => {\n const runId = c.req.param('runId');\n const body = ReplayRequestSchema.safeParse(await safelyParseJson(c));\n if (!body.success) {\n return c.json(buildValidationError('Invalid replay body.', body.error.issues), 400);\n }\n const mode: ReplayMode = body.data.raw === true ? 'raw' : 'sanitized';\n const auth = c.get('state').auth;\n const scopeError = enforceReplayScope(auth, mode);\n if (scopeError !== undefined) {\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:skipped',\n target: `run:${runId}`,\n decision: 'denied',\n ts: now(),\n metadata: { mode, reason: scopeError.code },\n });\n return c.json(scopeError.body, scopeError.status);\n }\n const replay = await deps.replay.loadRunReplay({\n runId,\n mode,\n ...(body.data.fromMessageId !== undefined ? { fromMessageId: body.data.fromMessageId } : {}),\n ...(body.data.provider !== undefined ? { provider: body.data.provider } : {}),\n });\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:accessed',\n target: `run:${runId}`,\n decision: 'success',\n ts: now(),\n metadata: { mode, eventCount: replay.events.length },\n });\n return c.json(serializeReplay(replay, mode));\n });\n\n app.post('/sessions/:id/replay', async (c) => {\n const sessionId = c.req.param('id');\n const body = ReplayRequestSchema.safeParse(await safelyParseJson(c));\n if (!body.success) {\n return c.json(buildValidationError('Invalid replay body.', body.error.issues), 400);\n }\n const mode: ReplayMode = body.data.raw === true ? 'raw' : 'sanitized';\n const auth = c.get('state').auth;\n const scopeError = enforceReplayScope(auth, mode);\n if (scopeError !== undefined) {\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:skipped',\n target: `session:${sessionId}`,\n decision: 'denied',\n ts: now(),\n metadata: { mode, reason: scopeError.code },\n });\n return c.json(scopeError.body, scopeError.status);\n }\n const replay = await deps.replay.loadSessionReplay({\n sessionId,\n mode,\n ...(body.data.fromMessageId !== undefined ? { fromMessageId: body.data.fromMessageId } : {}),\n ...(body.data.provider !== undefined ? { provider: body.data.provider } : {}),\n });\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:accessed',\n target: `session:${sessionId}`,\n decision: 'success',\n ts: now(),\n metadata: { mode, eventCount: replay.events.length },\n });\n return c.json(serializeReplay(replay, mode));\n });\n\n return app;\n}\n\ninterface ScopeRefusal {\n readonly code: 'auth-required' | 'scope-denied';\n readonly status: ContentfulStatusCode;\n readonly body: { readonly error: string; readonly message: string; readonly hint?: string };\n}\n\nfunction enforceReplayScope(auth: AuthState, mode: ReplayMode): ScopeRefusal | undefined {\n // IP-13: both a verified token and the no-auth anonymous principal carry a\n // `grantedScopes` set; only a genuinely unauthenticated request is refused.\n if (auth.kind === 'unauthenticated') {\n return {\n code: 'auth-required',\n status: 401,\n body: {\n error: 'auth-required',\n message: 'Authentication required for replay endpoints.',\n },\n };\n }\n const required = mode === 'raw' ? SCOPE_RAW : SCOPE_SANITIZED;\n for (const scope of auth.grantedScopes) {\n if (scopeMatches(scope, required)) return undefined;\n }\n return {\n code: 'scope-denied',\n status: 403,\n body: {\n error: 'scope-denied',\n message: `Token lacks required scope '${required.raw}'.`,\n hint: `Mint a token with the '${required.raw}' (or admin:*) scope.`,\n },\n };\n}\n\nfunction auditActor(auth: AuthState): {\n readonly kind: 'token' | 'system';\n readonly id: string;\n readonly label?: string;\n} {\n if (auth.kind === 'token') {\n return {\n kind: 'token',\n id: auth.token.tokenId,\n ...(auth.token.label !== undefined ? { label: auth.token.label } : {}),\n };\n }\n // IP-13: attribute no-auth replays to the synthetic 'anonymous' principal.\n return { kind: 'system', id: auth.kind === 'anonymous' ? 'anonymous' : 'unauthenticated' };\n}\n\nasync function maybeAppendAudit(\n auditDb: AuditDb | undefined,\n entry: Parameters<typeof appendAudit>[1],\n): Promise<void> {\n if (auditDb === undefined) return;\n try {\n await appendAudit(auditDb, entry);\n } catch {\n // Best-effort — replay must not be blocked by audit failures.\n }\n}\n\nfunction serializeReplay(replay: ReplayResponse, mode: ReplayMode): Record<string, unknown> {\n return {\n mode,\n events: [...replay.events],\n ...(replay.truncated === true ? { truncated: true } : {}),\n ...(replay.nextCursor !== undefined ? { nextCursor: replay.nextCursor } : {}),\n };\n}\n\nfunction buildValidationError(\n message: string,\n issues: ReadonlyArray<{\n readonly path: ReadonlyArray<string | number>;\n readonly message: string;\n }>,\n): Record<string, unknown> {\n return {\n error: 'config-invalid',\n message,\n issues: issues.map((i) => ({ path: i.path, message: i.message })),\n };\n}\n\nasync function safelyParseJson(c: {\n readonly req: { readonly json: () => Promise<unknown> };\n}): 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":"routes.js","names":["SCOPE_SANITIZED: ParsedScope","SCOPE_RAW: ParsedScope","mode: ReplayMode"],"sources":["../../src/replay/routes.ts"],"sourcesContent":["/**\n * Replay endpoints with scope enforcement.\n *\n * POST /v1/runs/:runId/replay\n * POST /v1/sessions/:id/replay\n *\n * Scope rules:\n * - `raw: false` (default) → require `traces:read:sanitized`.\n * - `raw: true` → require `traces:read:raw` (admin).\n *\n * Every successful invocation appends a `trace.replay.accessed` audit\n * entry. The audit write is best-effort: a missing `AuditDb` does not\n * block the replay.\n *\n * The actual replay stream is consumer-supplied - the framework\n * accepts a `ReplayApi` from the operator that knows how to load\n * trace events for a `runId` / `sessionId`. Phase 14c only owns the\n * scope check + audit + transport.\n *\n * @packageDocumentation\n */\n\nimport { type AuditDb, appendAudit } from '@graphorin/security/audit';\nimport { type ParsedScope, parseScope, scopeMatches } from '@graphorin/security/auth';\nimport { Hono } from 'hono';\nimport type { ContentfulStatusCode } from 'hono/utils/http-status';\nimport { z } from 'zod';\n\nimport type { AuthState, ServerVariables } from '../internal/context.js';\n\n/** @stable */\nexport type ReplayMode = 'sanitized' | 'raw';\n\n/** @stable */\nexport interface ReplayApi {\n loadRunReplay(input: {\n readonly runId: string;\n readonly mode: ReplayMode;\n readonly fromMessageId?: string;\n readonly provider?: string;\n }): Promise<ReplayResponse>;\n loadSessionReplay(input: {\n readonly sessionId: string;\n readonly mode: ReplayMode;\n readonly fromMessageId?: string;\n readonly provider?: string;\n }): Promise<ReplayResponse>;\n}\n\n/** @stable */\nexport interface ReplayResponse {\n readonly events: ReadonlyArray<unknown>;\n readonly truncated?: boolean;\n readonly nextCursor?: string;\n}\n\nconst SCOPE_SANITIZED: ParsedScope = parseScope('traces:read:sanitized');\nconst SCOPE_RAW: ParsedScope = parseScope('traces:read:raw');\n\nconst ReplayRequestSchema = z\n .object({\n raw: z.boolean().optional(),\n fromMessageId: z.string().min(1).optional(),\n provider: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport interface ReplayRoutesDeps {\n readonly replay: ReplayApi;\n readonly auditDb?: AuditDb;\n readonly now?: () => number;\n}\n\n/**\n * @stable\n */\nexport function createReplayRoutes(deps: ReplayRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const now = deps.now ?? Date.now;\n\n app.post('/runs/:runId/replay', async (c) => {\n const runId = c.req.param('runId');\n const body = ReplayRequestSchema.safeParse(await safelyParseJson(c));\n if (!body.success) {\n return c.json(buildValidationError('Invalid replay body.', body.error.issues), 400);\n }\n const mode: ReplayMode = body.data.raw === true ? 'raw' : 'sanitized';\n const auth = c.get('state').auth;\n const scopeError = enforceReplayScope(auth, mode);\n if (scopeError !== undefined) {\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:skipped',\n target: `run:${runId}`,\n decision: 'denied',\n ts: now(),\n metadata: { mode, reason: scopeError.code },\n });\n return c.json(scopeError.body, scopeError.status);\n }\n const replay = await deps.replay.loadRunReplay({\n runId,\n mode,\n ...(body.data.fromMessageId !== undefined ? { fromMessageId: body.data.fromMessageId } : {}),\n ...(body.data.provider !== undefined ? { provider: body.data.provider } : {}),\n });\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:accessed',\n target: `run:${runId}`,\n decision: 'success',\n ts: now(),\n metadata: { mode, eventCount: replay.events.length },\n });\n return c.json(serializeReplay(replay, mode));\n });\n\n app.post('/sessions/:id/replay', async (c) => {\n const sessionId = c.req.param('id');\n const body = ReplayRequestSchema.safeParse(await safelyParseJson(c));\n if (!body.success) {\n return c.json(buildValidationError('Invalid replay body.', body.error.issues), 400);\n }\n const mode: ReplayMode = body.data.raw === true ? 'raw' : 'sanitized';\n const auth = c.get('state').auth;\n const scopeError = enforceReplayScope(auth, mode);\n if (scopeError !== undefined) {\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:skipped',\n target: `session:${sessionId}`,\n decision: 'denied',\n ts: now(),\n metadata: { mode, reason: scopeError.code },\n });\n return c.json(scopeError.body, scopeError.status);\n }\n const replay = await deps.replay.loadSessionReplay({\n sessionId,\n mode,\n ...(body.data.fromMessageId !== undefined ? { fromMessageId: body.data.fromMessageId } : {}),\n ...(body.data.provider !== undefined ? { provider: body.data.provider } : {}),\n });\n await maybeAppendAudit(deps.auditDb, {\n actor: auditActor(auth),\n action: 'replay:accessed',\n target: `session:${sessionId}`,\n decision: 'success',\n ts: now(),\n metadata: { mode, eventCount: replay.events.length },\n });\n return c.json(serializeReplay(replay, mode));\n });\n\n return app;\n}\n\ninterface ScopeRefusal {\n readonly code: 'auth-required' | 'scope-denied';\n readonly status: ContentfulStatusCode;\n readonly body: { readonly error: string; readonly message: string; readonly hint?: string };\n}\n\nfunction enforceReplayScope(auth: AuthState, mode: ReplayMode): ScopeRefusal | undefined {\n // IP-13: both a verified token and the no-auth anonymous principal carry a\n // `grantedScopes` set; only a genuinely unauthenticated request is refused.\n if (auth.kind === 'unauthenticated') {\n return {\n code: 'auth-required',\n status: 401,\n body: {\n error: 'auth-required',\n message: 'Authentication required for replay endpoints.',\n },\n };\n }\n const required = mode === 'raw' ? SCOPE_RAW : SCOPE_SANITIZED;\n for (const scope of auth.grantedScopes) {\n if (scopeMatches(scope, required)) return undefined;\n }\n return {\n code: 'scope-denied',\n status: 403,\n body: {\n error: 'scope-denied',\n message: `Token lacks required scope '${required.raw}'.`,\n hint: `Mint a token with the '${required.raw}' (or admin:*) scope.`,\n },\n };\n}\n\nfunction auditActor(auth: AuthState): {\n readonly kind: 'token' | 'system';\n readonly id: string;\n readonly label?: string;\n} {\n if (auth.kind === 'token') {\n return {\n kind: 'token',\n id: auth.token.tokenId,\n ...(auth.token.label !== undefined ? { label: auth.token.label } : {}),\n };\n }\n // IP-13: attribute no-auth replays to the synthetic 'anonymous' principal.\n return { kind: 'system', id: auth.kind === 'anonymous' ? 'anonymous' : 'unauthenticated' };\n}\n\nasync function maybeAppendAudit(\n auditDb: AuditDb | undefined,\n entry: Parameters<typeof appendAudit>[1],\n): Promise<void> {\n if (auditDb === undefined) return;\n try {\n await appendAudit(auditDb, entry);\n } catch {\n // Best-effort - replay must not be blocked by audit failures.\n }\n}\n\nfunction serializeReplay(replay: ReplayResponse, mode: ReplayMode): Record<string, unknown> {\n return {\n mode,\n events: [...replay.events],\n ...(replay.truncated === true ? { truncated: true } : {}),\n ...(replay.nextCursor !== undefined ? { nextCursor: replay.nextCursor } : {}),\n };\n}\n\nfunction buildValidationError(\n message: string,\n issues: ReadonlyArray<{\n readonly path: ReadonlyArray<string | number>;\n readonly message: string;\n }>,\n): Record<string, unknown> {\n return {\n error: 'config-invalid',\n message,\n issues: issues.map((i) => ({ path: i.path, message: i.message })),\n };\n}\n\nasync function safelyParseJson(c: {\n readonly req: { readonly json: () => Promise<unknown> };\n}): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAMA,kBAA+B,WAAW,wBAAwB;AACxE,MAAMC,YAAyB,WAAW,kBAAkB;AAE5D,MAAM,sBAAsB,EACzB,OAAO;CACN,KAAK,EAAE,SAAS,CAAC,UAAU;CAC3B,eAAe,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CAC3C,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACvC,CAAC,CACD,QAAQ,CACR,QAAQ,EAAE,CAAC;;;;AAcd,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;CACtD,MAAM,MAAM,KAAK,OAAO,KAAK;AAE7B,KAAI,KAAK,uBAAuB,OAAO,MAAM;EAC3C,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ;EAClC,MAAM,OAAO,oBAAoB,UAAU,MAAM,gBAAgB,EAAE,CAAC;AACpE,MAAI,CAAC,KAAK,QACR,QAAO,EAAE,KAAK,qBAAqB,wBAAwB,KAAK,MAAM,OAAO,EAAE,IAAI;EAErF,MAAMC,OAAmB,KAAK,KAAK,QAAQ,OAAO,QAAQ;EAC1D,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAC5B,MAAM,aAAa,mBAAmB,MAAM,KAAK;AACjD,MAAI,eAAe,QAAW;AAC5B,SAAM,iBAAiB,KAAK,SAAS;IACnC,OAAO,WAAW,KAAK;IACvB,QAAQ;IACR,QAAQ,OAAO;IACf,UAAU;IACV,IAAI,KAAK;IACT,UAAU;KAAE;KAAM,QAAQ,WAAW;KAAM;IAC5C,CAAC;AACF,UAAO,EAAE,KAAK,WAAW,MAAM,WAAW,OAAO;;EAEnD,MAAM,SAAS,MAAM,KAAK,OAAO,cAAc;GAC7C;GACA;GACA,GAAI,KAAK,KAAK,kBAAkB,SAAY,EAAE,eAAe,KAAK,KAAK,eAAe,GAAG,EAAE;GAC3F,GAAI,KAAK,KAAK,aAAa,SAAY,EAAE,UAAU,KAAK,KAAK,UAAU,GAAG,EAAE;GAC7E,CAAC;AACF,QAAM,iBAAiB,KAAK,SAAS;GACnC,OAAO,WAAW,KAAK;GACvB,QAAQ;GACR,QAAQ,OAAO;GACf,UAAU;GACV,IAAI,KAAK;GACT,UAAU;IAAE;IAAM,YAAY,OAAO,OAAO;IAAQ;GACrD,CAAC;AACF,SAAO,EAAE,KAAK,gBAAgB,QAAQ,KAAK,CAAC;GAC5C;AAEF,KAAI,KAAK,wBAAwB,OAAO,MAAM;EAC5C,MAAM,YAAY,EAAE,IAAI,MAAM,KAAK;EACnC,MAAM,OAAO,oBAAoB,UAAU,MAAM,gBAAgB,EAAE,CAAC;AACpE,MAAI,CAAC,KAAK,QACR,QAAO,EAAE,KAAK,qBAAqB,wBAAwB,KAAK,MAAM,OAAO,EAAE,IAAI;EAErF,MAAMA,OAAmB,KAAK,KAAK,QAAQ,OAAO,QAAQ;EAC1D,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAC5B,MAAM,aAAa,mBAAmB,MAAM,KAAK;AACjD,MAAI,eAAe,QAAW;AAC5B,SAAM,iBAAiB,KAAK,SAAS;IACnC,OAAO,WAAW,KAAK;IACvB,QAAQ;IACR,QAAQ,WAAW;IACnB,UAAU;IACV,IAAI,KAAK;IACT,UAAU;KAAE;KAAM,QAAQ,WAAW;KAAM;IAC5C,CAAC;AACF,UAAO,EAAE,KAAK,WAAW,MAAM,WAAW,OAAO;;EAEnD,MAAM,SAAS,MAAM,KAAK,OAAO,kBAAkB;GACjD;GACA;GACA,GAAI,KAAK,KAAK,kBAAkB,SAAY,EAAE,eAAe,KAAK,KAAK,eAAe,GAAG,EAAE;GAC3F,GAAI,KAAK,KAAK,aAAa,SAAY,EAAE,UAAU,KAAK,KAAK,UAAU,GAAG,EAAE;GAC7E,CAAC;AACF,QAAM,iBAAiB,KAAK,SAAS;GACnC,OAAO,WAAW,KAAK;GACvB,QAAQ;GACR,QAAQ,WAAW;GACnB,UAAU;GACV,IAAI,KAAK;GACT,UAAU;IAAE;IAAM,YAAY,OAAO,OAAO;IAAQ;GACrD,CAAC;AACF,SAAO,EAAE,KAAK,gBAAgB,QAAQ,KAAK,CAAC;GAC5C;AAEF,QAAO;;AAST,SAAS,mBAAmB,MAAiB,MAA4C;AAGvF,KAAI,KAAK,SAAS,kBAChB,QAAO;EACL,MAAM;EACN,QAAQ;EACR,MAAM;GACJ,OAAO;GACP,SAAS;GACV;EACF;CAEH,MAAM,WAAW,SAAS,QAAQ,YAAY;AAC9C,MAAK,MAAM,SAAS,KAAK,cACvB,KAAI,aAAa,OAAO,SAAS,CAAE,QAAO;AAE5C,QAAO;EACL,MAAM;EACN,QAAQ;EACR,MAAM;GACJ,OAAO;GACP,SAAS,+BAA+B,SAAS,IAAI;GACrD,MAAM,0BAA0B,SAAS,IAAI;GAC9C;EACF;;AAGH,SAAS,WAAW,MAIlB;AACA,KAAI,KAAK,SAAS,QAChB,QAAO;EACL,MAAM;EACN,IAAI,KAAK,MAAM;EACf,GAAI,KAAK,MAAM,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,OAAO,GAAG,EAAE;EACtE;AAGH,QAAO;EAAE,MAAM;EAAU,IAAI,KAAK,SAAS,cAAc,cAAc;EAAmB;;AAG5F,eAAe,iBACb,SACA,OACe;AACf,KAAI,YAAY,OAAW;AAC3B,KAAI;AACF,QAAM,YAAY,SAAS,MAAM;SAC3B;;AAKV,SAAS,gBAAgB,QAAwB,MAA2C;AAC1F,QAAO;EACL;EACA,QAAQ,CAAC,GAAG,OAAO,OAAO;EAC1B,GAAI,OAAO,cAAc,OAAO,EAAE,WAAW,MAAM,GAAG,EAAE;EACxD,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,YAAY,GAAG,EAAE;EAC7E;;AAGH,SAAS,qBACP,SACA,QAIyB;AACzB,QAAO;EACL,OAAO;EACP;EACA,QAAQ,OAAO,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,SAAS,EAAE;GAAS,EAAE;EAClE;;AAGH,eAAe,gBAAgB,GAEV;AACnB,KAAI;AACF,SAAO,MAAM,EAAE,IAAI,MAAM;SACnB;AACN,SAAO,EAAE"}
@@ -195,7 +195,7 @@ function createRunRoutes(deps) {
195
195
  error: "resume-not-implemented",
196
196
  runId,
197
197
  status: state.status,
198
- message: "Server-side HITL resume is not implemented yet. Resume library-side: agent.run(result.state, { directive }) the suspended RunState is on the AgentResult."
198
+ message: "Server-side HITL resume is not implemented yet. Resume library-side: agent.run(result.state, { directive }) - the suspended RunState is on the AgentResult."
199
199
  }, 501);
200
200
  });
201
201
  return app;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","names":["callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string }"],"sources":["../../src/routes/agents.ts"],"sourcesContent":["/**\n * Agent REST routes.\n *\n * GET /agents (scope `agents:read`)\n * GET /agents/:id (scope `agents:read`)\n * POST /agents/:id/run (idempotent; scope `agents:invoke[:id]`)\n * POST /agents/:id/stream (scope `agents:invoke[:id]`)\n * GET /runs/:runId/state (scope `agents:read`)\n * POST /runs/:runId/abort (scope `agents:invoke`)\n * POST /runs/:runId/resume (idempotent; scope `agents:invoke`)\n *\n * Agents are looked up via {@link AgentRegistry}; missing entries\n * surface a 404 with a typed error body.\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\nimport { AgentNotFoundError } 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 { AgentRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface AgentRoutesDeps {\n readonly agents: AgentRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /**\n * Streaming dispatcher (IP-2). When present, `POST /:id/stream`\n * actually runs the agent and emits every event onto the\n * `agent:<id>/runs/<runId>/events` subject.\n */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst RunBodySchema = z\n .object({\n input: z.unknown().optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport function createAgentRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('agents:read'), (c) => c.json({ agents: deps.agents.list() }));\n\n app.get(\n '/:id',\n createScopeMiddleware((_path, params) => `agents:read:${params.id}`),\n (c) => {\n const id = c.req.param('id');\n const summary = deps.agents.describe(id);\n if (summary === undefined) {\n return c.json(\n { error: 'agent-not-found', message: `Agent '${id}' is not registered.` },\n 404,\n );\n }\n return c.json(summary);\n },\n );\n\n app.post(\n '/:id/run',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const agent = deps.agents.get(id);\n if (agent === undefined) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid run 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 sessionId = parsed.data.sessionId;\n const userId = parsed.data.userId;\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n try {\n const callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string } = {\n signal: tracker.signal,\n };\n if (sessionId !== undefined) callOptions.sessionId = sessionId;\n if (userId !== undefined) callOptions.userId = userId;\n const result = await agent.run(parsed.data.input ?? '', callOptions);\n deps.runs.complete(runId, 'completed');\n return c.json({ runId, status: 'completed', result }, 200);\n } catch (err) {\n const aborted = tracker.signal.aborted;\n deps.runs.complete(runId, aborted ? 'aborted' : 'failed', err);\n return c.json(\n {\n runId,\n status: aborted ? 'aborted' : 'failed',\n error: aborted ? 'run-aborted' : 'run-failed',\n message: err instanceof Error ? err.message : String(err),\n },\n aborted ? 408 : 500,\n );\n }\n },\n );\n\n app.post(\n '/:id/stream',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n if (!deps.agents.has(id)) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const runId = newRunId();\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n const sessionId = parsed.success ? parsed.data.sessionId : undefined;\n const userId = parsed.success ? parsed.data.userId : undefined;\n const input = parsed.success ? (parsed.data.input ?? '') : '';\n // IP-2: actually run the agent. The old handler parsed the input\n // and threw it away — the run sat 'pending' forever while the 202\n // advertised subjects nothing would ever emit on.\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n const subject = `agent:${id}/runs/${runId}/events`;\n const agent = deps.agents.get(id);\n if (agent !== undefined) {\n backgroundStreamAgent(agent, input, {\n signal: tracker.signal,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n runs: deps.runs,\n runId,\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n }\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // The SSE URL previously advertised here pointed at a path\n // that was never mounted — the WebSocket subject is the\n // delivery channel (IP-2).\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\n/**\n * IP-2: consume the agent's event stream in the background and emit\n * every event onto the run subject. Falls back to `run(...)` (a single\n * terminal frame) for registry entries without a `stream` surface.\n */\nfunction backgroundStreamAgent(\n agent: ReturnType<AgentRegistry['get']> & object,\n input: unknown,\n opts: {\n readonly signal: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n readonly runs: RunStateTracker;\n readonly runId: string;\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const emit = (type: string, payload: unknown): void => {\n opts.dispatcher?.emit(opts.subject, { type, payload });\n };\n void (async () => {\n try {\n const callOpts = {\n signal: opts.signal,\n ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),\n ...(opts.userId !== undefined ? { userId: opts.userId } : {}),\n };\n if (typeof agent.stream === 'function') {\n for await (const ev of agent.stream(input, callOpts)) {\n if (opts.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'agent.event';\n emit(type, ev);\n }\n } else {\n const output = await agent.run(input, callOpts);\n emit('agent.end', { runId: opts.runId, output });\n }\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n emit('agent.error', {\n runId: opts.runId,\n message: err instanceof Error ? err.message : String(err),\n });\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * Companion router for the `/runs/...` surface. Kept separate so the\n * `createServer` factory can mount it under the top-level base path\n * rather than under `/agents`.\n *\n * @stable\n */\nexport function createRunRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/:runId/state', createScopeMiddleware('agents:read'), (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json(state);\n });\n\n app.post('/:runId/abort', createScopeMiddleware('agents:invoke'), (c) => {\n const runId = c.req.param('runId');\n const aborted = deps.runs.abort(runId);\n if (!aborted) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json({ runId, status: 'aborted' });\n });\n\n app.post('/:runId/resume', createScopeMiddleware('agents:invoke'), async (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n // IP-14: a 202 that persists nothing and resumes nothing is a lie\n // the client SDK was built on. Until the server can rehydrate the\n // RunState and re-enter the agent loop (the persisted-approvals\n // work — Wave 3), the honest answer is 501 with the working\n // library-side path: `agent.run(result.state, { directive })`\n // executes approved tools for real (AG-1/AG-9).\n return c.json(\n {\n error: 'resume-not-implemented',\n runId,\n status: state.status,\n message:\n 'Server-side HITL resume is not implemented yet. Resume library-side: ' +\n 'agent.run(result.state, { directive }) — the suspended RunState is on the AgentResult.',\n },\n 501,\n );\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":"agents.js","names":["callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string }"],"sources":["../../src/routes/agents.ts"],"sourcesContent":["/**\n * Agent REST routes.\n *\n * GET /agents (scope `agents:read`)\n * GET /agents/:id (scope `agents:read`)\n * POST /agents/:id/run (idempotent; scope `agents:invoke[:id]`)\n * POST /agents/:id/stream (scope `agents:invoke[:id]`)\n * GET /runs/:runId/state (scope `agents:read`)\n * POST /runs/:runId/abort (scope `agents:invoke`)\n * POST /runs/:runId/resume (idempotent; scope `agents:invoke`)\n *\n * Agents are looked up via {@link AgentRegistry}; missing entries\n * surface a 404 with a typed error body.\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\nimport { AgentNotFoundError } 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 { AgentRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface AgentRoutesDeps {\n readonly agents: AgentRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /**\n * Streaming dispatcher (IP-2). When present, `POST /:id/stream`\n * actually runs the agent and emits every event onto the\n * `agent:<id>/runs/<runId>/events` subject.\n */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst RunBodySchema = z\n .object({\n input: z.unknown().optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport function createAgentRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('agents:read'), (c) => c.json({ agents: deps.agents.list() }));\n\n app.get(\n '/:id',\n createScopeMiddleware((_path, params) => `agents:read:${params.id}`),\n (c) => {\n const id = c.req.param('id');\n const summary = deps.agents.describe(id);\n if (summary === undefined) {\n return c.json(\n { error: 'agent-not-found', message: `Agent '${id}' is not registered.` },\n 404,\n );\n }\n return c.json(summary);\n },\n );\n\n app.post(\n '/:id/run',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const agent = deps.agents.get(id);\n if (agent === undefined) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid run 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 sessionId = parsed.data.sessionId;\n const userId = parsed.data.userId;\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n try {\n const callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string } = {\n signal: tracker.signal,\n };\n if (sessionId !== undefined) callOptions.sessionId = sessionId;\n if (userId !== undefined) callOptions.userId = userId;\n const result = await agent.run(parsed.data.input ?? '', callOptions);\n deps.runs.complete(runId, 'completed');\n return c.json({ runId, status: 'completed', result }, 200);\n } catch (err) {\n const aborted = tracker.signal.aborted;\n deps.runs.complete(runId, aborted ? 'aborted' : 'failed', err);\n return c.json(\n {\n runId,\n status: aborted ? 'aborted' : 'failed',\n error: aborted ? 'run-aborted' : 'run-failed',\n message: err instanceof Error ? err.message : String(err),\n },\n aborted ? 408 : 500,\n );\n }\n },\n );\n\n app.post(\n '/:id/stream',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n if (!deps.agents.has(id)) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const runId = newRunId();\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n const sessionId = parsed.success ? parsed.data.sessionId : undefined;\n const userId = parsed.success ? parsed.data.userId : undefined;\n const input = parsed.success ? (parsed.data.input ?? '') : '';\n // IP-2: actually run the agent. The old handler parsed the input\n // and threw it away - the run sat 'pending' forever while the 202\n // advertised subjects nothing would ever emit on.\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n const subject = `agent:${id}/runs/${runId}/events`;\n const agent = deps.agents.get(id);\n if (agent !== undefined) {\n backgroundStreamAgent(agent, input, {\n signal: tracker.signal,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n runs: deps.runs,\n runId,\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n }\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // The SSE URL previously advertised here pointed at a path\n // that was never mounted - the WebSocket subject is the\n // delivery channel (IP-2).\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\n/**\n * IP-2: consume the agent's event stream in the background and emit\n * every event onto the run subject. Falls back to `run(...)` (a single\n * terminal frame) for registry entries without a `stream` surface.\n */\nfunction backgroundStreamAgent(\n agent: ReturnType<AgentRegistry['get']> & object,\n input: unknown,\n opts: {\n readonly signal: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n readonly runs: RunStateTracker;\n readonly runId: string;\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const emit = (type: string, payload: unknown): void => {\n opts.dispatcher?.emit(opts.subject, { type, payload });\n };\n void (async () => {\n try {\n const callOpts = {\n signal: opts.signal,\n ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),\n ...(opts.userId !== undefined ? { userId: opts.userId } : {}),\n };\n if (typeof agent.stream === 'function') {\n for await (const ev of agent.stream(input, callOpts)) {\n if (opts.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'agent.event';\n emit(type, ev);\n }\n } else {\n const output = await agent.run(input, callOpts);\n emit('agent.end', { runId: opts.runId, output });\n }\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n emit('agent.error', {\n runId: opts.runId,\n message: err instanceof Error ? err.message : String(err),\n });\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * Companion router for the `/runs/...` surface. Kept separate so the\n * `createServer` factory can mount it under the top-level base path\n * rather than under `/agents`.\n *\n * @stable\n */\nexport function createRunRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/:runId/state', createScopeMiddleware('agents:read'), (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json(state);\n });\n\n app.post('/:runId/abort', createScopeMiddleware('agents:invoke'), (c) => {\n const runId = c.req.param('runId');\n const aborted = deps.runs.abort(runId);\n if (!aborted) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json({ runId, status: 'aborted' });\n });\n\n app.post('/:runId/resume', createScopeMiddleware('agents:invoke'), async (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n // IP-14: a 202 that persists nothing and resumes nothing is a lie\n // the client SDK was built on. Until the server can rehydrate the\n // RunState and re-enter the agent loop (the persisted-approvals\n // work - Wave 3), the honest answer is 501 with the working\n // library-side path: `agent.run(result.state, { directive })`\n // executes approved tools for real (AG-1/AG-9).\n return c.json(\n {\n error: 'resume-not-implemented',\n runId,\n status: state.status,\n message:\n 'Server-side HITL resume is not implemented yet. Resume library-side: ' +\n 'agent.run(result.state, { directive }) - the suspended RunState is on the AgentResult.',\n },\n 501,\n );\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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"}
@@ -28,7 +28,7 @@ interface AuditApi {
28
28
  /**
29
29
  * Verify the chain integrity of every audit row in the inclusive
30
30
  * range. Phase 14c surfaces this through `POST /v1/audit/verify`.
31
- * Optional operators that opt out of the audit chain should
31
+ * Optional - operators that opt out of the audit chain should
32
32
  * leave this method off.
33
33
  */
34
34
  verify?(opts: {
@@ -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
  */