@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.
- package/CHANGELOG.md +71 -0
- package/README.md +3 -3
- package/dist/app-audit-binding.d.ts +15 -0
- package/dist/app-audit-binding.d.ts.map +1 -0
- package/dist/app-audit-binding.js +136 -0
- package/dist/app-audit-binding.js.map +1 -0
- package/dist/app-daemons.d.ts +29 -0
- package/dist/app-daemons.d.ts.map +1 -0
- package/dist/app-daemons.js +27 -0
- package/dist/app-daemons.js.map +1 -0
- package/dist/app-lifecycle.js +272 -0
- package/dist/app-lifecycle.js.map +1 -0
- package/dist/app-metrics.js +79 -0
- package/dist/app-metrics.js.map +1 -0
- package/dist/app-middleware.js +92 -0
- package/dist/app-middleware.js.map +1 -0
- package/dist/app-routes.js +131 -0
- package/dist/app-routes.js.map +1 -0
- package/dist/app-ws.js +65 -0
- package/dist/app-ws.js.map +1 -0
- package/dist/app.d.ts +16 -16
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +35 -607
- package/dist/app.js.map +1 -1
- package/dist/commentary/built-in-patterns.d.ts +1 -1
- package/dist/commentary/built-in-patterns.js +1 -1
- package/dist/commentary/built-in-patterns.js.map +1 -1
- package/dist/config.d.ts +100 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -2
- package/dist/config.js.map +1 -1
- package/dist/health/checks.d.ts +15 -1
- package/dist/health/checks.d.ts.map +1 -1
- package/dist/health/checks.js +21 -0
- package/dist/health/checks.js.map +1 -1
- package/dist/health/routes.js +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -24
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +2 -2
- package/dist/internal/wire-error.js +20 -0
- package/dist/internal/wire-error.js.map +1 -0
- package/dist/lifecycle/pre-bind.d.ts +1 -1
- package/dist/metrics/catalog.d.ts.map +1 -1
- package/dist/metrics/catalog.js.map +1 -1
- package/dist/metrics/tools-bridge.d.ts +15 -0
- package/dist/metrics/tools-bridge.d.ts.map +1 -0
- package/dist/metrics/tools-bridge.js +103 -0
- package/dist/metrics/tools-bridge.js.map +1 -0
- package/dist/middleware/audit.d.ts +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/csrf.js +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/scope.d.ts.map +1 -1
- package/dist/middleware/scope.js +44 -4
- package/dist/middleware/scope.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/index.d.ts +33 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/replay/routes.d.ts +1 -1
- package/dist/replay/routes.js +2 -2
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +64 -18
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.js +5 -5
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/tokens.d.ts +1 -1
- package/dist/routes/tokens.d.ts.map +1 -1
- package/dist/routes/tokens.js +21 -1
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +165 -19
- package/dist/routes/workflows.js.map +1 -1
- package/dist/runtime/retention.d.ts +105 -0
- package/dist/runtime/retention.d.ts.map +1 -0
- package/dist/runtime/retention.js +154 -0
- package/dist/runtime/retention.js.map +1 -0
- package/dist/runtime/run-state.d.ts +2 -0
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +34 -2
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/sse/events.js +3 -4
- package/dist/sse/events.js.map +1 -1
- package/dist/tools-audit-bridge.d.ts +29 -0
- package/dist/tools-audit-bridge.d.ts.map +1 -0
- package/dist/tools-audit-bridge.js +103 -0
- package/dist/tools-audit-bridge.js.map +1 -0
- package/dist/workflows/timer-daemon.d.ts +69 -0
- package/dist/workflows/timer-daemon.d.ts.map +1 -0
- package/dist/workflows/timer-daemon.js +37 -0
- package/dist/workflows/timer-daemon.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +1 -1
- package/dist/ws/dispatcher.js +19 -12
- package/dist/ws/dispatcher.js.map +1 -1
- package/dist/ws/index.d.ts +2 -2
- package/dist/ws/index.js +3 -3
- package/dist/ws/replay-buffer.d.ts +35 -1
- package/dist/ws/replay-buffer.d.ts.map +1 -1
- package/dist/ws/replay-buffer.js +35 -3
- package/dist/ws/replay-buffer.js.map +1 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/dist/ws/upgrade.js +24 -18
- package/dist/ws/upgrade.js.map +1 -1
- package/package.json +30 -29
- package/src/app-audit-binding.ts +227 -0
- package/src/app-daemons.ts +73 -0
- package/src/app-lifecycle.ts +476 -0
- package/src/app-metrics.ts +144 -0
- package/src/app-middleware.ts +149 -0
- package/src/app-routes.ts +305 -0
- package/src/app-ws.ts +111 -0
- package/src/app.ts +317 -0
- package/src/commentary/audit-bridge.ts +135 -0
- package/src/commentary/built-in-patterns.ts +79 -0
- package/src/commentary/index.ts +32 -0
- package/src/commentary/sanitizer.ts +238 -0
- package/src/commentary/types.ts +130 -0
- package/src/config.ts +472 -0
- package/src/consolidator/daemon.ts +141 -0
- package/src/consolidator/index.ts +14 -0
- package/src/errors/index.ts +247 -0
- package/src/health/checks.ts +322 -0
- package/src/health/index.ts +34 -0
- package/src/health/routes.ts +154 -0
- package/src/index.ts +243 -0
- package/src/internal/context.ts +77 -0
- package/src/internal/ids.ts +17 -0
- package/src/internal/json.ts +47 -0
- package/src/internal/wire-error.ts +44 -0
- package/src/lifecycle/hooks.ts +66 -0
- package/src/lifecycle/index.ts +16 -0
- package/src/lifecycle/pre-bind.ts +138 -0
- package/src/metrics/catalog.ts +112 -0
- package/src/metrics/index.ts +12 -0
- package/src/metrics/registry.ts +337 -0
- package/src/metrics/tools-bridge.ts +124 -0
- package/src/middleware/audit.ts +114 -0
- package/src/middleware/auth.ts +195 -0
- package/src/middleware/cors.ts +72 -0
- package/src/middleware/csrf.ts +98 -0
- package/src/middleware/idempotency.ts +336 -0
- package/src/middleware/index.ts +38 -0
- package/src/middleware/rate-limit.ts +71 -0
- package/src/middleware/request-state.ts +79 -0
- package/src/middleware/scope.ts +161 -0
- package/src/registry/index.ts +278 -0
- package/src/replay/index.ts +14 -0
- package/src/replay/routes.ts +255 -0
- package/src/routes/agents.ts +363 -0
- package/src/routes/audit.ts +207 -0
- package/src/routes/auth.ts +80 -0
- package/src/routes/health.ts +42 -0
- package/src/routes/index.ts +16 -0
- package/src/routes/mcp.ts +97 -0
- package/src/routes/memory.ts +152 -0
- package/src/routes/sessions.ts +250 -0
- package/src/routes/skills.ts +91 -0
- package/src/routes/tokens.ts +166 -0
- package/src/routes/workflows.ts +616 -0
- package/src/runtime/retention.ts +284 -0
- package/src/runtime/run-state.ts +422 -0
- package/src/sse/events.ts +303 -0
- package/src/sse/index.ts +7 -0
- package/src/tools-audit-bridge.ts +120 -0
- package/src/triggers/daemon.ts +198 -0
- package/src/triggers/index.ts +16 -0
- package/src/triggers/routes.ts +139 -0
- package/src/workflows/timer-daemon.ts +102 -0
- package/src/ws/dispatcher.ts +537 -0
- package/src/ws/index.ts +45 -0
- package/src/ws/replay-buffer.ts +209 -0
- package/src/ws/subjects.ts +162 -0
- package/src/ws/ticket.ts +174 -0
- package/src/ws/upgrade.ts +507 -0
- package/dist/lifecycle/index.js +0 -3
- 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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"}
|
package/dist/routes/tokens.d.ts
CHANGED
|
@@ -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":";;;;;;;;;;
|
|
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"}
|
package/dist/routes/tokens.js
CHANGED
|
@@ -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":"
|
|
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":";;;;;;;AA6BA,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;EAIH,MAAM,eAAe,iBAAiB,OAAO,KAAK,OAAO;AACzD,MAAI,aAAa,SAAS,EACxB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACT,QAAQ,aAAa,KAAK,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE;GAC1D,EACD,IACD;EASH,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;AAC5B,MAAI,KAAK,SAAS,SAAS;GACzB,MAAM,SAAS,OAAO,KAAK,OAAO,QAAQ,cAAc;IACtD,MAAM,kBAAkB,cAAc,UAAU;AAChD,QAAI,oBAAoB,OAAW,QAAO;AAC1C,WAAO,CAAC,gBAAgB,KAAK,eAAe,gBAAgB;KAC5D;AACF,OAAI,OAAO,SAAS,EAClB,QAAO,EAAE,KACP;IACE,OAAO;IACP,SACE;IACF;IACD,EACD,IACD;;EAGL,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflows.d.ts","names":[],"sources":["../../src/routes/workflows.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;
|
|
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"}
|
package/dist/routes/workflows.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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-
|
|
180
|
-
message:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
215
|
-
* path
|
|
216
|
-
*
|
|
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
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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"}
|