@graphorin/server 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +3 -3
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +35 -607
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +8 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +19 -24
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +1 -1
  59. package/dist/package.js.map +1 -1
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.js +19 -12
  99. package/dist/ws/dispatcher.js.map +1 -1
  100. package/dist/ws/index.d.ts +2 -2
  101. package/dist/ws/index.js +3 -3
  102. package/dist/ws/replay-buffer.d.ts +35 -1
  103. package/dist/ws/replay-buffer.d.ts.map +1 -1
  104. package/dist/ws/replay-buffer.js +35 -3
  105. package/dist/ws/replay-buffer.js.map +1 -1
  106. package/dist/ws/upgrade.d.ts.map +1 -1
  107. package/dist/ws/upgrade.js +24 -18
  108. package/dist/ws/upgrade.js.map +1 -1
  109. package/package.json +30 -29
  110. package/src/app-audit-binding.ts +227 -0
  111. package/src/app-daemons.ts +73 -0
  112. package/src/app-lifecycle.ts +476 -0
  113. package/src/app-metrics.ts +144 -0
  114. package/src/app-middleware.ts +149 -0
  115. package/src/app-routes.ts +305 -0
  116. package/src/app-ws.ts +111 -0
  117. package/src/app.ts +317 -0
  118. package/src/commentary/audit-bridge.ts +135 -0
  119. package/src/commentary/built-in-patterns.ts +79 -0
  120. package/src/commentary/index.ts +32 -0
  121. package/src/commentary/sanitizer.ts +238 -0
  122. package/src/commentary/types.ts +130 -0
  123. package/src/config.ts +472 -0
  124. package/src/consolidator/daemon.ts +141 -0
  125. package/src/consolidator/index.ts +14 -0
  126. package/src/errors/index.ts +247 -0
  127. package/src/health/checks.ts +322 -0
  128. package/src/health/index.ts +34 -0
  129. package/src/health/routes.ts +154 -0
  130. package/src/index.ts +243 -0
  131. package/src/internal/context.ts +77 -0
  132. package/src/internal/ids.ts +17 -0
  133. package/src/internal/json.ts +47 -0
  134. package/src/internal/wire-error.ts +44 -0
  135. package/src/lifecycle/hooks.ts +66 -0
  136. package/src/lifecycle/index.ts +16 -0
  137. package/src/lifecycle/pre-bind.ts +138 -0
  138. package/src/metrics/catalog.ts +112 -0
  139. package/src/metrics/index.ts +12 -0
  140. package/src/metrics/registry.ts +337 -0
  141. package/src/metrics/tools-bridge.ts +124 -0
  142. package/src/middleware/audit.ts +114 -0
  143. package/src/middleware/auth.ts +195 -0
  144. package/src/middleware/cors.ts +72 -0
  145. package/src/middleware/csrf.ts +98 -0
  146. package/src/middleware/idempotency.ts +336 -0
  147. package/src/middleware/index.ts +38 -0
  148. package/src/middleware/rate-limit.ts +71 -0
  149. package/src/middleware/request-state.ts +79 -0
  150. package/src/middleware/scope.ts +161 -0
  151. package/src/registry/index.ts +278 -0
  152. package/src/replay/index.ts +14 -0
  153. package/src/replay/routes.ts +255 -0
  154. package/src/routes/agents.ts +363 -0
  155. package/src/routes/audit.ts +207 -0
  156. package/src/routes/auth.ts +80 -0
  157. package/src/routes/health.ts +42 -0
  158. package/src/routes/index.ts +16 -0
  159. package/src/routes/mcp.ts +97 -0
  160. package/src/routes/memory.ts +152 -0
  161. package/src/routes/sessions.ts +250 -0
  162. package/src/routes/skills.ts +91 -0
  163. package/src/routes/tokens.ts +166 -0
  164. package/src/routes/workflows.ts +616 -0
  165. package/src/runtime/retention.ts +284 -0
  166. package/src/runtime/run-state.ts +422 -0
  167. package/src/sse/events.ts +303 -0
  168. package/src/sse/index.ts +7 -0
  169. package/src/tools-audit-bridge.ts +120 -0
  170. package/src/triggers/daemon.ts +198 -0
  171. package/src/triggers/index.ts +16 -0
  172. package/src/triggers/routes.ts +139 -0
  173. package/src/workflows/timer-daemon.ts +102 -0
  174. package/src/ws/dispatcher.ts +537 -0
  175. package/src/ws/index.ts +45 -0
  176. package/src/ws/replay-buffer.ts +209 -0
  177. package/src/ws/subjects.ts +162 -0
  178. package/src/ws/ticket.ts +174 -0
  179. package/src/ws/upgrade.ts +507 -0
  180. package/dist/lifecycle/index.js +0 -3
  181. package/dist/routes/index.js +0 -12
@@ -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 // W-107: per-resource requirement - a token scoped to ONE session\n // reads it on every surface (WS subjects already gated per-resource;\n // wide two-segment grants keep covering three-segment requirements).\n app.get(\n '/:id',\n createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),\n 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\n app.delete(\n '/:id',\n createScopeMiddleware((_path, params) => `sessions:write:${params.id}`),\n 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\n app.get(\n '/:id/messages',\n createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),\n 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\n app.get(\n '/:id/handoffs',\n createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),\n 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\n app.post(\n '/:id/export',\n createScopeMiddleware((_path, params) => `sessions:export:${params.id}`),\n 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\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,6 +1,6 @@
1
1
  import { ServerVariables } from "../internal/context.js";
2
- import { SecretValue } from "@graphorin/security";
3
2
  import { Hono } from "hono";
3
+ import { SecretValue } from "@graphorin/security";
4
4
  import { AuthTokenStore } from "@graphorin/core/contracts";
5
5
 
6
6
  //#region src/routes/tokens.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","names":[],"sources":["../../src/routes/tokens.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAwCgE,UAnB/C,gBAAA,CAmB+C;uBAlBzC;mBACJ;;wBAEK;;;;;iBAeR,kBAAA,OAAyB,mBAAmB;aAAkB"}
1
+ {"version":3,"file":"tokens.d.ts","names":[],"sources":["../../src/routes/tokens.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAyCgE,UAnB/C,gBAAA,CAmB+C;uBAlBzC;mBACJ;;wBAEK;;;;;iBAeR,kBAAA,OAAyB,mBAAmB;aAAkB"}
@@ -1,6 +1,7 @@
1
1
  import { createScopeMiddleware } from "../middleware/scope.js";
2
- import { createToken, listTokens, revokeToken } from "@graphorin/security";
3
2
  import { Hono } from "hono";
3
+ import { createToken, listTokens, revokeToken } from "@graphorin/security";
4
+ import { scopeSetMatches, tryParseScope, validateScopeSet } from "@graphorin/security/auth";
4
5
  import { z } from "zod";
5
6
 
6
7
  //#region src/routes/tokens.ts
@@ -43,6 +44,25 @@ function createTokensRoutes(deps) {
43
44
  message: `Environment '${env}' is not in the allowed set.`,
44
45
  allowed: deps.allowedEnvs
45
46
  }, 400);
47
+ const syntaxErrors = validateScopeSet(parsed.data.scopes);
48
+ if (syntaxErrors.length > 0) return c.json({
49
+ error: "config-invalid",
50
+ message: "One or more requested scopes are syntactically invalid.",
51
+ issues: syntaxErrors.map((e) => ({ message: e.message }))
52
+ }, 400);
53
+ const auth = c.get("state").auth;
54
+ if (auth.kind === "token") {
55
+ const denied = parsed.data.scopes.filter((requested) => {
56
+ const requestedParsed = tryParseScope(requested);
57
+ if (requestedParsed === void 0) return true;
58
+ return !scopeSetMatches(auth.grantedScopes, requestedParsed);
59
+ });
60
+ if (denied.length > 0) return c.json({
61
+ error: "scope-escalation-denied",
62
+ message: "Attenuation-only minting: a token may only mint scopes covered by its own grant.",
63
+ denied
64
+ }, 403);
65
+ }
46
66
  const created = await createToken({
47
67
  tokenStore: deps.tokenStore,
48
68
  pepper: deps.pepper,
@@ -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 { scopeSetMatches, tryParseScope, validateScopeSet } from '@graphorin/security/auth';\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 // W-106: reject syntactically-invalid scopes up front - the old\n // z.string().min(3) accepted garbage that would grant nothing.\n const syntaxErrors = validateScopeSet(parsed.data.scopes);\n if (syntaxErrors.length > 0) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'One or more requested scopes are syntactically invalid.',\n issues: syntaxErrors.map((e) => ({ message: e.message })),\n },\n 400,\n );\n }\n // W-106: attenuation-only minting. A token principal can only mint\n // scopes its OWN grant already covers - `tokens:create` alone must\n // not transitively zero-out the least-privilege catalogue by minting\n // `admin:*`. Delegation chains therefore narrow monotonically (the\n // child's `tokens:create` must itself be covered). The anonymous\n // principal (IP-13 trusted loopback operator, auth disabled) is\n // exempt: it already holds `admin:*`.\n const auth = c.get('state').auth;\n if (auth.kind === 'token') {\n const denied = parsed.data.scopes.filter((requested) => {\n const requestedParsed = tryParseScope(requested);\n if (requestedParsed === undefined) return true;\n return !scopeSetMatches(auth.grantedScopes, requestedParsed);\n });\n if (denied.length > 0) {\n return c.json(\n {\n error: 'scope-escalation-denied',\n message:\n 'Attenuation-only minting: a token may only mint scopes covered by its own grant.',\n denied,\n },\n 403,\n );\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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"workflows.d.ts","names":[],"sources":["../../src/routes/workflows.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UA0BiB,kBAAA;sBACK;iBACL;;;wBAAe;;;;;iBAiChB,oBAAA,OACR,qBACL;aAAkB"}
1
+ {"version":3,"file":"workflows.d.ts","names":[],"sources":["../../src/routes/workflows.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UA+BiB,kBAAA;sBACK;iBACL;;;wBAAe;;;;;iBA6ChB,oBAAA,OACR,qBACL;aAAkB"}
@@ -1,6 +1,7 @@
1
- import { WorkflowNotFoundError } from "../errors/index.js";
2
- import { createScopeMiddleware } from "../middleware/scope.js";
3
1
  import { newRequestId } from "../internal/ids.js";
2
+ import { createScopeMiddleware } from "../middleware/scope.js";
3
+ import { WorkflowNotFoundError } from "../errors/index.js";
4
+ import { toWireError } from "../internal/wire-error.js";
4
5
  import { Hono } from "hono";
5
6
  import { z } from "zod";
6
7
 
@@ -13,8 +14,10 @@ const ExecuteBodySchema = z.object({
13
14
  }).strict().default({});
14
15
  const ResumeBodySchema = z.object({
15
16
  threadId: z.string().min(1),
16
- resume: z.unknown().optional()
17
+ resume: z.unknown().optional(),
18
+ name: z.string().min(1).optional()
17
19
  }).strict();
20
+ const ThreadBodySchema = z.object({ threadId: z.string().min(1) }).strict();
18
21
  const ForkBodySchema = z.object({
19
22
  fromThreadId: z.string().min(1),
20
23
  fromCheckpointId: z.string().min(1).optional()
@@ -83,7 +86,11 @@ function createWorkflowRoutes(deps) {
83
86
  message: i.message
84
87
  }))
85
88
  }, 400);
86
- if (workflow.resume === void 0) return c.json({
89
+ if (parsed.data.name !== void 0 && workflow.resolveAwakeable === void 0) return c.json({
90
+ error: "workflow-resume-unsupported",
91
+ message: `Workflow '${id}' does not implement resolveAwakeable().`
92
+ }, 400);
93
+ if (parsed.data.name === void 0 && workflow.resume === void 0) return c.json({
87
94
  error: "workflow-resume-unsupported",
88
95
  message: `Workflow '${id}' does not implement resume().`
89
96
  }, 400);
@@ -105,6 +112,87 @@ function createWorkflowRoutes(deps) {
105
112
  subscribe: { websocket: subject }
106
113
  }, 202);
107
114
  });
115
+ app.post("/:id/retry", createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`), async (c) => {
116
+ const id = c.req.param("id");
117
+ const workflow = deps.workflows.get(id);
118
+ if (workflow === void 0) {
119
+ const err = new WorkflowNotFoundError(id);
120
+ return c.json({
121
+ error: err.kind,
122
+ message: err.message
123
+ }, 404);
124
+ }
125
+ const parsed = ThreadBodySchema.safeParse(await safelyParseJson(c));
126
+ if (!parsed.success) return c.json({
127
+ error: "config-invalid",
128
+ message: "Invalid retry body.",
129
+ issues: parsed.error.issues.map((i) => ({
130
+ path: i.path,
131
+ message: i.message
132
+ }))
133
+ }, 400);
134
+ if (workflow.retry === void 0) return c.json({
135
+ error: "workflow-retry-unsupported",
136
+ message: `Workflow '${id}' does not implement retry().`
137
+ }, 400);
138
+ const runId = newRunId();
139
+ const tracker = deps.runs.start(runId, {
140
+ kind: "workflow",
141
+ workflowId: id,
142
+ threadId: parsed.data.threadId
143
+ });
144
+ const subject = `workflow:${id}/runs/${runId}/events`;
145
+ const retryFn = workflow.retry.bind(workflow);
146
+ backgroundIterate(() => retryFn(parsed.data.threadId, { signal: tracker.signal }), tracker, deps.runs, runId, {
147
+ subject,
148
+ ...deps.dispatcher !== void 0 ? { dispatcher: deps.dispatcher } : {}
149
+ });
150
+ return c.json({
151
+ runId,
152
+ threadId: parsed.data.threadId,
153
+ status: "running",
154
+ subscribe: { websocket: subject }
155
+ }, 202);
156
+ });
157
+ app.post("/:id/tick", createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`), async (c) => {
158
+ const id = c.req.param("id");
159
+ const workflow = deps.workflows.get(id);
160
+ if (workflow === void 0) {
161
+ const err = new WorkflowNotFoundError(id);
162
+ return c.json({
163
+ error: err.kind,
164
+ message: err.message
165
+ }, 404);
166
+ }
167
+ const parsed = ThreadBodySchema.safeParse(await safelyParseJson(c));
168
+ if (!parsed.success) return c.json({
169
+ error: "config-invalid",
170
+ message: "Invalid tick body.",
171
+ issues: parsed.error.issues.map((i) => ({
172
+ path: i.path,
173
+ message: i.message
174
+ }))
175
+ }, 400);
176
+ if (workflow.tick === void 0) return c.json({
177
+ error: "workflow-tick-unsupported",
178
+ message: `Workflow '${id}' does not implement tick().`
179
+ }, 400);
180
+ try {
181
+ const result = await workflow.tick(parsed.data.threadId);
182
+ return c.json({
183
+ workflowId: id,
184
+ threadId: parsed.data.threadId,
185
+ fired: result.fired,
186
+ nextWakeAt: result.nextWakeAt
187
+ });
188
+ } catch (err) {
189
+ const wire = toWireError(err);
190
+ return c.json({
191
+ error: wire.code,
192
+ message: wire.message
193
+ }, wire.code === "thread-not-found" ? 404 : 400);
194
+ }
195
+ });
108
196
  app.get("/:id/state", createScopeMiddleware((_path, params) => `workflows:read:${params.id}`), async (c) => {
109
197
  const id = c.req.param("id");
110
198
  const workflow = deps.workflows.get(id);
@@ -157,9 +245,28 @@ function createWorkflowRoutes(deps) {
157
245
  checkpoints
158
246
  });
159
247
  });
248
+ app.delete("/:id/threads/:threadId", createScopeMiddleware((_path, params) => `workflows:delete:${params.id}`), async (c) => {
249
+ const id = c.req.param("id");
250
+ const threadId = c.req.param("threadId");
251
+ const workflow = deps.workflows.get(id);
252
+ if (workflow === void 0) {
253
+ const err = new WorkflowNotFoundError(id);
254
+ return c.json({
255
+ error: err.kind,
256
+ message: err.message
257
+ }, 404);
258
+ }
259
+ if (workflow.deleteThread === void 0) return c.json({
260
+ error: "workflow-delete-thread-unsupported",
261
+ message: "Workflow does not implement deleteThread()."
262
+ }, 400);
263
+ await workflow.deleteThread(threadId);
264
+ return c.body(null, 204);
265
+ });
160
266
  app.post("/:id/fork", createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`), async (c) => {
161
267
  const id = c.req.param("id");
162
- if (deps.workflows.get(id) === void 0) {
268
+ const workflow = deps.workflows.get(id);
269
+ if (workflow === void 0) {
163
270
  const err = new WorkflowNotFoundError(id);
164
271
  return c.json({
165
272
  error: err.kind,
@@ -175,12 +282,38 @@ function createWorkflowRoutes(deps) {
175
282
  message: i.message
176
283
  }))
177
284
  }, 400);
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.",
181
- workflowId: id,
182
- fork: parsed.data
183
- }, 501);
285
+ if (workflow.fork === void 0) return c.json({
286
+ error: "workflow-fork-unsupported",
287
+ message: `Workflow '${id}' does not implement fork().`
288
+ }, 400);
289
+ try {
290
+ let fromCheckpointId = parsed.data.fromCheckpointId;
291
+ if (fromCheckpointId === void 0) {
292
+ if (workflow.getState === void 0) return c.json({
293
+ error: "config-invalid",
294
+ message: "fromCheckpointId is required: this workflow does not expose getState() to derive the latest checkpoint."
295
+ }, 400);
296
+ const state = await workflow.getState(parsed.data.fromThreadId);
297
+ if (typeof state?.checkpointId !== "string") return c.json({
298
+ error: "checkpoint-not-found",
299
+ message: `Thread '${parsed.data.fromThreadId}' has no checkpoint to fork from.`
300
+ }, 404);
301
+ fromCheckpointId = state.checkpointId;
302
+ }
303
+ const forked = await workflow.fork(parsed.data.fromThreadId, fromCheckpointId);
304
+ return c.json({
305
+ workflowId: id,
306
+ fromThreadId: parsed.data.fromThreadId,
307
+ fromCheckpointId,
308
+ newThreadId: forked.newThreadId
309
+ }, 201);
310
+ } catch (err) {
311
+ const wire = toWireError(err);
312
+ return c.json({
313
+ error: wire.code,
314
+ message: wire.message
315
+ }, wire.code === "thread-not-found" || wire.code === "checkpoint-not-found" ? 404 : 400);
316
+ }
184
317
  });
185
318
  return app;
186
319
  }
@@ -203,7 +336,7 @@ function backgroundExecute(workflow, body, tracker, runs, runId, streaming) {
203
336
  type: "workflow.error",
204
337
  payload: {
205
338
  runId,
206
- message: err instanceof Error ? err.message : String(err)
339
+ ...toWireError(err)
207
340
  }
208
341
  });
209
342
  runs.complete(runId, tracker.signal.aborted ? "aborted" : "failed", err);
@@ -211,16 +344,29 @@ function backgroundExecute(workflow, body, tracker, runs, runId, streaming) {
211
344
  })();
212
345
  }
213
346
  /**
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.
347
+ * periphery-01 / W-119: mirror of {@link backgroundExecute} for the
348
+ * resume path. A `name` in the body routes through
349
+ * `resolveAwakeable(threadId, name, resume)`; a plain resume goes
350
+ * through `resume(threadId, {resume})`. Both receive the tracker's
351
+ * AbortSignal so `runs.abort(runId)` actually cancels the iteration
352
+ * (parity with backgroundExecute - the old path dropped it).
217
353
  */
218
354
  function backgroundResume(workflow, body, tracker, runs, runId, streaming) {
219
- const resume = workflow.resume;
220
- if (resume === void 0) return;
355
+ const { resume, resolveAwakeable } = workflow;
356
+ const name = body.name;
357
+ const factory = name !== void 0 && resolveAwakeable !== void 0 ? () => resolveAwakeable.call(workflow, body.threadId, name, body.resume, { signal: tracker.signal }) : resume !== void 0 ? () => resume.call(workflow, body.threadId, body.resume !== void 0 ? { resume: body.resume } : {}, { signal: tracker.signal }) : void 0;
358
+ if (factory === void 0) return;
359
+ backgroundIterate(factory, tracker, runs, runId, streaming);
360
+ }
361
+ /**
362
+ * W-119: shared background iteration for resume / named resume / retry -
363
+ * emit every event on the run subject, complete the tracked run, wrap
364
+ * failures in the W-052 wire envelope.
365
+ */
366
+ function backgroundIterate(factory, tracker, runs, runId, streaming) {
221
367
  (async () => {
222
368
  try {
223
- for await (const ev of resume.call(workflow, body.threadId, body.resume !== void 0 ? { resume: body.resume } : {})) {
369
+ for await (const ev of factory()) {
224
370
  if (tracker.signal.aborted) break;
225
371
  const type = typeof ev.type === "string" ? ev.type : "workflow.event";
226
372
  streaming.dispatcher?.emit(streaming.subject, {
@@ -234,7 +380,7 @@ function backgroundResume(workflow, body, tracker, runs, runId, streaming) {
234
380
  type: "workflow.error",
235
381
  payload: {
236
382
  runId,
237
- message: err instanceof Error ? err.message : String(err)
383
+ ...toWireError(err)
238
384
  }
239
385
  });
240
386
  runs.complete(runId, tracker.signal.aborted ? "aborted" : "failed", err);
@@ -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 // 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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"}
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 * `name` targets an awakeable/approval, W-119)\n * POST /workflows/:id/retry (scope `workflows:resume[:id]`, W-119)\n * POST /workflows/:id/tick (scope `workflows:resume[:id]`, W-119)\n * GET /workflows/:id/state (scope `workflows:read`)\n * GET /workflows/:id/checkpoints (scope `workflows:read`)\n * POST /workflows/:id/fork (scope `workflows:execute`; real fork, W-119)\n * DELETE /workflows/:id/threads/:threadId (scope `workflows:delete`)\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 { toWireError } from '../internal/wire-error.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 * W-119: awakeable/approval name. When present the resume routes\n * through `resolveAwakeable(threadId, name, resume)` - `approve` is\n * the same primitive, no alias route needed.\n */\n name: z.string().min(1).optional(),\n })\n .strict();\n\nconst ThreadBodySchema = z\n .object({\n threadId: z.string().min(1),\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 (parsed.data.name !== undefined && workflow.resolveAwakeable === undefined) {\n return c.json(\n {\n error: 'workflow-resume-unsupported',\n message: `Workflow '${id}' does not implement resolveAwakeable().`,\n },\n 400,\n );\n }\n if (parsed.data.name === undefined && 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 // W-119: replay a failed/aborted thread. Background-iterated like\n // resume; the run subject carries the events.\n app.post(\n '/:id/retry',\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 = ThreadBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid retry body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n if (workflow.retry === undefined) {\n return c.json(\n {\n error: 'workflow-retry-unsupported',\n message: `Workflow '${id}' does not implement retry().`,\n },\n 400,\n );\n }\n const runId = newRunId();\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 const retryFn = workflow.retry.bind(workflow);\n backgroundIterate(\n () => retryFn(parsed.data.threadId, { signal: tracker.signal }),\n tracker,\n deps.runs,\n runId,\n { subject, ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}) },\n );\n return c.json(\n {\n runId,\n threadId: parsed.data.threadId,\n status: 'running',\n subscribe: { websocket: subject },\n },\n 202,\n );\n },\n );\n\n // W-119: fire a due durable timer synchronously. Long node bodies\n // hold the HTTP connection - documented; wire the timer daemon\n // (createServer({ workflowTimers })) for regular firing instead.\n app.post(\n '/:id/tick',\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 = ThreadBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid tick body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n if (workflow.tick === undefined) {\n return c.json(\n {\n error: 'workflow-tick-unsupported',\n message: `Workflow '${id}' does not implement tick().`,\n },\n 400,\n );\n }\n try {\n const result = await workflow.tick(parsed.data.threadId);\n return c.json({\n workflowId: id,\n threadId: parsed.data.threadId,\n fired: result.fired,\n nextWakeAt: result.nextWakeAt,\n });\n } catch (err) {\n const wire = toWireError(err);\n return c.json(\n { error: wire.code, message: wire.message },\n wire.code === 'thread-not-found' ? 404 : 400,\n );\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 // W-005: the reachable per-thread erasure lever. 204 on success (the\n // store delete is idempotent - deleting an unknown thread is a no-op),\n // 404 for an unknown workflow, 400 when the registered entry does not\n // expose deleteThread.\n app.delete(\n '/:id/threads/:threadId',\n createScopeMiddleware((_path, params) => `workflows:delete:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const threadId = c.req.param('threadId');\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 if (workflow.deleteThread === undefined) {\n return c.json(\n {\n error: 'workflow-delete-thread-unsupported',\n message: 'Workflow does not implement deleteThread().',\n },\n 400,\n );\n }\n await workflow.deleteThread(threadId);\n return c.body(null, 204);\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 // W-119: the fork primitive is now threaded through the registry -\n // the periphery-01 honest-501 kept its promise and retires here.\n if (workflow.fork === undefined) {\n return c.json(\n {\n error: 'workflow-fork-unsupported',\n message: `Workflow '${id}' does not implement fork().`,\n },\n 400,\n );\n }\n try {\n let fromCheckpointId = parsed.data.fromCheckpointId;\n if (fromCheckpointId === undefined) {\n if (workflow.getState === undefined) {\n return c.json(\n {\n error: 'config-invalid',\n message:\n 'fromCheckpointId is required: this workflow does not expose getState() to derive the latest checkpoint.',\n },\n 400,\n );\n }\n const state = (await workflow.getState(parsed.data.fromThreadId)) as {\n readonly checkpointId?: unknown;\n };\n if (typeof state?.checkpointId !== 'string') {\n return c.json(\n {\n error: 'checkpoint-not-found',\n message: `Thread '${parsed.data.fromThreadId}' has no checkpoint to fork from.`,\n },\n 404,\n );\n }\n fromCheckpointId = state.checkpointId;\n }\n const forked = await workflow.fork(parsed.data.fromThreadId, fromCheckpointId);\n return c.json(\n {\n workflowId: id,\n fromThreadId: parsed.data.fromThreadId,\n fromCheckpointId,\n newThreadId: forked.newThreadId,\n },\n 201,\n );\n } catch (err) {\n const wire = toWireError(err);\n return c.json(\n { error: wire.code, message: wire.message },\n wire.code === 'thread-not-found' || wire.code === 'checkpoint-not-found' ? 404 : 400,\n );\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 // W-052: the frame carries the normalized machine-readable code\n // next to the unchanged message, so clients can retry\n // `checkpoint-version-conflict` and abandon\n // `node-execution-failed` without parsing prose.\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, ...toWireError(err) },\n });\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * periphery-01 / W-119: mirror of {@link backgroundExecute} for the\n * resume path. A `name` in the body routes through\n * `resolveAwakeable(threadId, name, resume)`; a plain resume goes\n * through `resume(threadId, {resume})`. Both receive the tracker's\n * AbortSignal so `runs.abort(runId)` actually cancels the iteration\n * (parity with backgroundExecute - the old path dropped it).\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, resolveAwakeable } = workflow;\n const name = body.name;\n const factory =\n name !== undefined && resolveAwakeable !== undefined\n ? () =>\n resolveAwakeable.call(workflow, body.threadId, name, body.resume, {\n signal: tracker.signal,\n })\n : resume !== undefined\n ? () =>\n resume.call(\n workflow,\n body.threadId,\n body.resume !== undefined ? { resume: body.resume } : {},\n { signal: tracker.signal },\n )\n : undefined;\n if (factory === undefined) return;\n backgroundIterate(factory, tracker, runs, runId, streaming);\n}\n\n/**\n * W-119: shared background iteration for resume / named resume / retry -\n * emit every event on the run subject, complete the tracked run, wrap\n * failures in the W-052 wire envelope.\n */\nfunction backgroundIterate(\n factory: () => AsyncIterable<unknown>,\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 void (async () => {\n try {\n for await (const ev of factory()) {\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 // W-052: same envelope as the execute path.\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, ...toWireError(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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"}
@@ -0,0 +1,105 @@
1
+ import { SqliteConnection } from "@graphorin/store-sqlite";
2
+
3
+ //#region src/runtime/retention.d.ts
4
+
5
+ /**
6
+ * Mirror of the `config.retention` section of
7
+ * `ServerConfigSpec`.
8
+ *
9
+ * @stable
10
+ */
11
+ interface RetentionConfig {
12
+ readonly enabled: boolean;
13
+ readonly intervalMs: number;
14
+ readonly spansDays: number;
15
+ readonly consolidatorRunsDays: number;
16
+ readonly dlqExhaustedDays: number;
17
+ readonly idempotency: boolean;
18
+ readonly sessionsDays?: number;
19
+ readonly sessionsClosedOnly: boolean;
20
+ readonly memoryHistoryDays?: number;
21
+ readonly workflowThreadsDays?: number;
22
+ readonly auditDays?: number;
23
+ }
24
+ /**
25
+ * Structural slice of `GraphorinSqliteStore` the sweep consumes. Typed
26
+ * structurally so custom store implementations (and test fakes) work;
27
+ * a surface whose method is absent is skipped, never an error.
28
+ *
29
+ * @stable
30
+ */
31
+ interface RetentionStoreLike {
32
+ readonly connection: SqliteConnection;
33
+ readonly sessions: {
34
+ pruneSessions(opts: {
35
+ readonly beforeEpochMs?: number;
36
+ readonly closedOnly?: boolean;
37
+ }): Promise<number>;
38
+ pruneAuditEntries(beforeEpochMs: number): Promise<number>;
39
+ };
40
+ readonly memory: {
41
+ pruneHistory(olderThanMs: number): Promise<number>;
42
+ };
43
+ readonly checkpoints: {
44
+ pruneThreads(opts: {
45
+ readonly beforeEpochMs: number;
46
+ readonly onlyTerminal?: boolean;
47
+ }): Promise<number>;
48
+ };
49
+ readonly idempotency: {
50
+ prune(olderThan: number): Promise<number>;
51
+ };
52
+ }
53
+ /** @stable */
54
+ type RetentionLogLevel = 'info' | 'warn';
55
+ /**
56
+ * Logging seam: `warn` on a failed surface, `info` with per-surface
57
+ * deletion counts after each sweep. Defaults to a no-op.
58
+ *
59
+ * @stable
60
+ */
61
+ type RetentionLog = (level: RetentionLogLevel, message: string, fields?: Record<string, unknown>) => void;
62
+ /** @stable */
63
+ interface ScheduleRetentionOptions {
64
+ readonly store: RetentionStoreLike;
65
+ readonly config: RetentionConfig;
66
+ readonly now: () => number;
67
+ readonly log?: RetentionLog;
68
+ /**
69
+ * Test seam for the span sweep (the real one issues SQL against
70
+ * `store.connection`).
71
+ *
72
+ * @internal
73
+ */
74
+ readonly pruneSpansImpl?: (conn: SqliteConnection, opts: {
75
+ readonly beforeEpochMs: number;
76
+ }) => number;
77
+ }
78
+ /**
79
+ * Console-backed {@link RetentionLog} honouring the
80
+ * `observability.logger` config flavour. Returns `undefined` for
81
+ * `'silent'` so callers can pass the result straight through.
82
+ *
83
+ * @stable
84
+ */
85
+ declare function createConsoleRetentionLog(flavour: 'json' | 'pretty' | 'silent'): RetentionLog | undefined;
86
+ /**
87
+ * W-010: schedule the periodic retention sweep. Same lifecycle shape
88
+ * as `scheduleRunPruning`: `unref`-ed `setInterval` + a stop function.
89
+ * The FIRST sweep runs immediately on scheduling - a server that is
90
+ * restarted more often than `intervalMs` would otherwise never prune
91
+ * anything. Each surface is isolated in its own try/catch: one failing
92
+ * prune logs a WARN and never blocks the others. Overlapping sweeps
93
+ * are skipped (the previous sweep still running when the timer fires
94
+ * again is a signal the interval is too tight, not a reason to pile
95
+ * up writers).
96
+ *
97
+ * Returns a stop function; with `config.enabled === false` no timer is
98
+ * created and the stop function is a no-op.
99
+ *
100
+ * @stable
101
+ */
102
+ declare function scheduleRetentionSweeps(options: ScheduleRetentionOptions): () => void;
103
+ //#endregion
104
+ export { RetentionConfig, RetentionLog, RetentionLogLevel, RetentionStoreLike, ScheduleRetentionOptions, createConsoleRetentionLog, scheduleRetentionSweeps };
105
+ //# sourceMappingURL=retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.d.ts","names":[],"sources":["../../src/runtime/retention.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UA6CiB,eAAA;;;;;;;;;;;;;;;;;;;;UAqBA,kBAAA;uBACM;;;;;QAKf;8CACsC;;;uCAGP;;;;;;QAM/B;;;8BAGsB;;;;KAKlB,iBAAA;;;;;;;KAQA,YAAA,WACH,6CAEE;;UAIM,wBAAA;kBACC;mBACC;;iBAEF;;;;;;;mCAQP;;;;;;;;;;;iBAYM,yBAAA,yCAEb;;;;;;;;;;;;;;;;;iBAsDa,uBAAA,UAAiC"}