@graphorin/server 0.5.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 (204) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/dist/app.d.ts +174 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +684 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/commentary/audit-bridge.d.ts +57 -0
  9. package/dist/commentary/audit-bridge.d.ts.map +1 -0
  10. package/dist/commentary/audit-bridge.js +92 -0
  11. package/dist/commentary/audit-bridge.js.map +1 -0
  12. package/dist/commentary/built-in-patterns.d.ts +24 -0
  13. package/dist/commentary/built-in-patterns.d.ts.map +1 -0
  14. package/dist/commentary/built-in-patterns.js +62 -0
  15. package/dist/commentary/built-in-patterns.js.map +1 -0
  16. package/dist/commentary/index.d.ts +5 -0
  17. package/dist/commentary/index.js +5 -0
  18. package/dist/commentary/sanitizer.d.ts +37 -0
  19. package/dist/commentary/sanitizer.d.ts.map +1 -0
  20. package/dist/commentary/sanitizer.js +182 -0
  21. package/dist/commentary/sanitizer.js.map +1 -0
  22. package/dist/commentary/types.d.ts +120 -0
  23. package/dist/commentary/types.d.ts.map +1 -0
  24. package/dist/config.d.ts +760 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +217 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consolidator/daemon.d.ts +88 -0
  29. package/dist/consolidator/daemon.d.ts.map +1 -0
  30. package/dist/consolidator/daemon.js +54 -0
  31. package/dist/consolidator/daemon.js.map +1 -0
  32. package/dist/consolidator/index.d.ts +2 -0
  33. package/dist/consolidator/index.js +3 -0
  34. package/dist/errors/index.d.ts +104 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +131 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/health/checks.d.ts +120 -0
  39. package/dist/health/checks.d.ts.map +1 -0
  40. package/dist/health/checks.js +127 -0
  41. package/dist/health/checks.js.map +1 -0
  42. package/dist/health/index.d.ts +3 -0
  43. package/dist/health/index.js +4 -0
  44. package/dist/health/routes.d.ts +66 -0
  45. package/dist/health/routes.d.ts.map +1 -0
  46. package/dist/health/routes.js +96 -0
  47. package/dist/health/routes.js.map +1 -0
  48. package/dist/index.d.ts +88 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +86 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/context.d.ts +66 -0
  53. package/dist/internal/context.d.ts.map +1 -0
  54. package/dist/internal/ids.js +21 -0
  55. package/dist/internal/ids.js.map +1 -0
  56. package/dist/internal/json.js +46 -0
  57. package/dist/internal/json.js.map +1 -0
  58. package/dist/lifecycle/hooks.d.ts +57 -0
  59. package/dist/lifecycle/hooks.d.ts.map +1 -0
  60. package/dist/lifecycle/index.d.ts +2 -0
  61. package/dist/lifecycle/index.js +3 -0
  62. package/dist/lifecycle/pre-bind.d.ts +38 -0
  63. package/dist/lifecycle/pre-bind.d.ts.map +1 -0
  64. package/dist/lifecycle/pre-bind.js +62 -0
  65. package/dist/lifecycle/pre-bind.js.map +1 -0
  66. package/dist/metrics/catalog.d.ts +34 -0
  67. package/dist/metrics/catalog.d.ts.map +1 -0
  68. package/dist/metrics/catalog.js +61 -0
  69. package/dist/metrics/catalog.js.map +1 -0
  70. package/dist/metrics/index.d.ts +3 -0
  71. package/dist/metrics/index.js +4 -0
  72. package/dist/metrics/registry.d.ts +63 -0
  73. package/dist/metrics/registry.d.ts.map +1 -0
  74. package/dist/metrics/registry.js +222 -0
  75. package/dist/metrics/registry.js.map +1 -0
  76. package/dist/middleware/audit.d.ts +47 -0
  77. package/dist/middleware/audit.d.ts.map +1 -0
  78. package/dist/middleware/audit.js +71 -0
  79. package/dist/middleware/audit.js.map +1 -0
  80. package/dist/middleware/auth.d.ts +50 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +164 -0
  83. package/dist/middleware/auth.js.map +1 -0
  84. package/dist/middleware/cors.d.ts +15 -0
  85. package/dist/middleware/cors.d.ts.map +1 -0
  86. package/dist/middleware/cors.js +45 -0
  87. package/dist/middleware/cors.js.map +1 -0
  88. package/dist/middleware/csrf.d.ts +15 -0
  89. package/dist/middleware/csrf.d.ts.map +1 -0
  90. package/dist/middleware/csrf.js +77 -0
  91. package/dist/middleware/csrf.js.map +1 -0
  92. package/dist/middleware/idempotency.d.ts +41 -0
  93. package/dist/middleware/idempotency.d.ts.map +1 -0
  94. package/dist/middleware/idempotency.js +198 -0
  95. package/dist/middleware/idempotency.js.map +1 -0
  96. package/dist/middleware/index.d.ts +9 -0
  97. package/dist/middleware/index.js +10 -0
  98. package/dist/middleware/rate-limit.d.ts +17 -0
  99. package/dist/middleware/rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/rate-limit.js +47 -0
  101. package/dist/middleware/rate-limit.js.map +1 -0
  102. package/dist/middleware/request-state.d.ts +27 -0
  103. package/dist/middleware/request-state.d.ts.map +1 -0
  104. package/dist/middleware/request-state.js +42 -0
  105. package/dist/middleware/request-state.js.map +1 -0
  106. package/dist/middleware/scope.d.ts +24 -0
  107. package/dist/middleware/scope.d.ts.map +1 -0
  108. package/dist/middleware/scope.js +50 -0
  109. package/dist/middleware/scope.js.map +1 -0
  110. package/dist/registry/index.d.ts +135 -0
  111. package/dist/registry/index.d.ts.map +1 -0
  112. package/dist/registry/index.js +96 -0
  113. package/dist/registry/index.js.map +1 -0
  114. package/dist/replay/index.d.ts +2 -0
  115. package/dist/replay/index.js +3 -0
  116. package/dist/replay/routes.d.ts +46 -0
  117. package/dist/replay/routes.d.ts.map +1 -0
  118. package/dist/replay/routes.js +189 -0
  119. package/dist/replay/routes.js.map +1 -0
  120. package/dist/routes/agents.d.ts +41 -0
  121. package/dist/routes/agents.d.ts.map +1 -0
  122. package/dist/routes/agents.js +213 -0
  123. package/dist/routes/agents.js.map +1 -0
  124. package/dist/routes/audit.d.ts +57 -0
  125. package/dist/routes/audit.d.ts.map +1 -0
  126. package/dist/routes/audit.js +116 -0
  127. package/dist/routes/audit.js.map +1 -0
  128. package/dist/routes/auth.d.ts +26 -0
  129. package/dist/routes/auth.d.ts.map +1 -0
  130. package/dist/routes/auth.js +54 -0
  131. package/dist/routes/auth.js.map +1 -0
  132. package/dist/routes/health.d.ts +22 -0
  133. package/dist/routes/health.d.ts.map +1 -0
  134. package/dist/routes/health.js +33 -0
  135. package/dist/routes/health.js.map +1 -0
  136. package/dist/routes/index.d.ts +10 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/mcp.d.ts +32 -0
  139. package/dist/routes/mcp.d.ts.map +1 -0
  140. package/dist/routes/mcp.js +61 -0
  141. package/dist/routes/mcp.js.map +1 -0
  142. package/dist/routes/memory.d.ts +141 -0
  143. package/dist/routes/memory.d.ts.map +1 -0
  144. package/dist/routes/memory.js +102 -0
  145. package/dist/routes/memory.js.map +1 -0
  146. package/dist/routes/sessions.d.ts +54 -0
  147. package/dist/routes/sessions.d.ts.map +1 -0
  148. package/dist/routes/sessions.js +135 -0
  149. package/dist/routes/sessions.js.map +1 -0
  150. package/dist/routes/skills.d.ts +34 -0
  151. package/dist/routes/skills.d.ts.map +1 -0
  152. package/dist/routes/skills.js +54 -0
  153. package/dist/routes/skills.js.map +1 -0
  154. package/dist/routes/tokens.d.ts +25 -0
  155. package/dist/routes/tokens.d.ts.map +1 -0
  156. package/dist/routes/tokens.js +87 -0
  157. package/dist/routes/tokens.js.map +1 -0
  158. package/dist/routes/workflows.d.ts +27 -0
  159. package/dist/routes/workflows.d.ts.map +1 -0
  160. package/dist/routes/workflows.js +220 -0
  161. package/dist/routes/workflows.js.map +1 -0
  162. package/dist/runtime/run-state.d.ts +158 -0
  163. package/dist/runtime/run-state.d.ts.map +1 -0
  164. package/dist/runtime/run-state.js +182 -0
  165. package/dist/runtime/run-state.js.map +1 -0
  166. package/dist/sse/events.d.ts +44 -0
  167. package/dist/sse/events.d.ts.map +1 -0
  168. package/dist/sse/events.js +200 -0
  169. package/dist/sse/events.js.map +1 -0
  170. package/dist/sse/index.d.ts +2 -0
  171. package/dist/sse/index.js +3 -0
  172. package/dist/triggers/daemon.d.ts +74 -0
  173. package/dist/triggers/daemon.d.ts.map +1 -0
  174. package/dist/triggers/daemon.js +114 -0
  175. package/dist/triggers/daemon.js.map +1 -0
  176. package/dist/triggers/index.d.ts +3 -0
  177. package/dist/triggers/index.js +4 -0
  178. package/dist/triggers/routes.d.ts +21 -0
  179. package/dist/triggers/routes.d.ts.map +1 -0
  180. package/dist/triggers/routes.js +117 -0
  181. package/dist/triggers/routes.js.map +1 -0
  182. package/dist/ws/dispatcher.d.ts +169 -0
  183. package/dist/ws/dispatcher.d.ts.map +1 -0
  184. package/dist/ws/dispatcher.js +303 -0
  185. package/dist/ws/dispatcher.js.map +1 -0
  186. package/dist/ws/index.d.ts +6 -0
  187. package/dist/ws/index.js +7 -0
  188. package/dist/ws/replay-buffer.d.ts +47 -0
  189. package/dist/ws/replay-buffer.d.ts.map +1 -0
  190. package/dist/ws/replay-buffer.js +88 -0
  191. package/dist/ws/replay-buffer.js.map +1 -0
  192. package/dist/ws/subjects.d.ts +71 -0
  193. package/dist/ws/subjects.d.ts.map +1 -0
  194. package/dist/ws/subjects.js +112 -0
  195. package/dist/ws/subjects.js.map +1 -0
  196. package/dist/ws/ticket.d.ts +74 -0
  197. package/dist/ws/ticket.d.ts.map +1 -0
  198. package/dist/ws/ticket.js +112 -0
  199. package/dist/ws/ticket.js.map +1 -0
  200. package/dist/ws/upgrade.d.ts +63 -0
  201. package/dist/ws/upgrade.d.ts.map +1 -0
  202. package/dist/ws/upgrade.js +324 -0
  203. package/dist/ws/upgrade.js.map +1 -0
  204. package/package.json +156 -0
@@ -0,0 +1,54 @@
1
+ import { createScopeMiddleware } from "../middleware/scope.js";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+
5
+ //#region src/routes/skills.ts
6
+ const InstallBodySchema = z.object({
7
+ source: z.string().min(1),
8
+ trust: z.enum(["verified", "unverified"]).optional()
9
+ }).strict();
10
+ /**
11
+ * @stable
12
+ */
13
+ function createSkillsRoutes(deps) {
14
+ const app = new Hono();
15
+ app.get("/", createScopeMiddleware("skills:read"), async (c) => {
16
+ return c.json({ skills: await deps.skills.list() });
17
+ });
18
+ app.get("/:name", createScopeMiddleware("skills:read"), async (c) => {
19
+ const name = c.req.param("name");
20
+ const skill = await deps.skills.get(name);
21
+ if (skill === null) return c.json({
22
+ error: "skill-not-found",
23
+ message: `Skill '${name}' not found.`
24
+ }, 404);
25
+ return c.json({ skill });
26
+ });
27
+ app.post("/install", createScopeMiddleware("skills:install"), async (c) => {
28
+ const parsed = InstallBodySchema.safeParse(await safelyParseJson(c));
29
+ if (!parsed.success) return c.json({
30
+ error: "config-invalid",
31
+ message: "Invalid install body.",
32
+ issues: parsed.error.issues.map((i) => ({
33
+ path: i.path,
34
+ message: i.message
35
+ }))
36
+ }, 400);
37
+ const installInput = { source: parsed.data.source };
38
+ if (parsed.data.trust !== void 0) installInput.trust = parsed.data.trust;
39
+ const out = await deps.skills.install(installInput);
40
+ return c.json({ skill: out }, 201);
41
+ });
42
+ return app;
43
+ }
44
+ async function safelyParseJson(c) {
45
+ try {
46
+ return await c.req.json();
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ //#endregion
53
+ export { createSkillsRoutes };
54
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","names":["installInput: { source: string; trust?: 'verified' | 'unverified' }"],"sources":["../../src/routes/skills.ts"],"sourcesContent":["/**\n * Skill REST routes.\n *\n * GET /skills (scope `skills:read`)\n * POST /skills/install (idempotent; scope `skills:install`)\n * GET /skills/:name (scope `skills:read`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\n\n/**\n * @stable\n */\nexport interface SkillsApi {\n list(): Promise<ReadonlyArray<{ readonly name: string; readonly version?: string }>>;\n get(name: string): Promise<unknown | null>;\n install(input: {\n readonly source: string;\n readonly trust?: 'verified' | 'unverified';\n }): Promise<unknown>;\n}\n\nconst InstallBodySchema = z\n .object({\n source: z.string().min(1),\n trust: z.enum(['verified', 'unverified']).optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport interface SkillsRoutesDeps {\n readonly skills: SkillsApi;\n}\n\n/**\n * @stable\n */\nexport function createSkillsRoutes(deps: SkillsRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/', createScopeMiddleware('skills:read'), async (c) => {\n return c.json({ skills: await deps.skills.list() });\n });\n\n app.get('/:name', createScopeMiddleware('skills:read'), async (c) => {\n const name = c.req.param('name');\n const skill = await deps.skills.get(name);\n if (skill === null)\n return c.json({ error: 'skill-not-found', message: `Skill '${name}' not found.` }, 404);\n return c.json({ skill });\n });\n\n app.post('/install', createScopeMiddleware('skills:install'), async (c) => {\n const parsed = InstallBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid install body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const installInput: { source: string; trust?: 'verified' | 'unverified' } = {\n source: parsed.data.source,\n };\n if (parsed.data.trust !== undefined) installInput.trust = parsed.data.trust;\n const out = await deps.skills.install(installInput);\n return c.json({ skill: out }, 201);\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,oBAAoB,EACvB,OAAO;CACN,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE;CACzB,OAAO,EAAE,KAAK,CAAC,YAAY,aAAa,CAAC,CAAC,UAAU;CACrD,CAAC,CACD,QAAQ;;;;AAYX,SAAgB,mBAAmB,MAA8D;CAC/F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,IAAI,KAAK,sBAAsB,cAAc,EAAE,OAAO,MAAM;AAC9D,SAAO,EAAE,KAAK,EAAE,QAAQ,MAAM,KAAK,OAAO,MAAM,EAAE,CAAC;GACnD;AAEF,KAAI,IAAI,UAAU,sBAAsB,cAAc,EAAE,OAAO,MAAM;EACnE,MAAM,OAAO,EAAE,IAAI,MAAM,OAAO;EAChC,MAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,KAAK;AACzC,MAAI,UAAU,KACZ,QAAO,EAAE,KAAK;GAAE,OAAO;GAAmB,SAAS,UAAU,KAAK;GAAe,EAAE,IAAI;AACzF,SAAO,EAAE,KAAK,EAAE,OAAO,CAAC;GACxB;AAEF,KAAI,KAAK,YAAY,sBAAsB,iBAAiB,EAAE,OAAO,MAAM;EACzE,MAAM,SAAS,kBAAkB,UAAU,MAAM,gBAAgB,EAAE,CAAC;AACpE,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,MAAMA,eAAsE,EAC1E,QAAQ,OAAO,KAAK,QACrB;AACD,MAAI,OAAO,KAAK,UAAU,OAAW,cAAa,QAAQ,OAAO,KAAK;EACtE,MAAM,MAAM,MAAM,KAAK,OAAO,QAAQ,aAAa;AACnD,SAAO,EAAE,KAAK,EAAE,OAAO,KAAK,EAAE,IAAI;GAClC;AAEF,QAAO;;AAGT,eAAe,gBAAgB,GAA8D;AAC3F,KAAI;AACF,SAAO,MAAM,EAAE,IAAI,MAAM;SACnB;AACN,SAAO,EAAE"}
@@ -0,0 +1,25 @@
1
+ import { ServerVariables } from "../internal/context.js";
2
+ import { SecretValue } from "@graphorin/security";
3
+ import { Hono } from "hono";
4
+ import { AuthTokenStore } from "@graphorin/core/contracts";
5
+
6
+ //#region src/routes/tokens.d.ts
7
+
8
+ /**
9
+ * @stable
10
+ */
11
+ interface TokensRoutesDeps {
12
+ readonly tokenStore: AuthTokenStore;
13
+ readonly pepper: SecretValue;
14
+ readonly defaultEnv: string;
15
+ readonly allowedEnvs: ReadonlyArray<string>;
16
+ }
17
+ /**
18
+ * @stable
19
+ */
20
+ declare function createTokensRoutes(deps: TokensRoutesDeps): Hono<{
21
+ Variables: ServerVariables;
22
+ }>;
23
+ //#endregion
24
+ export { TokensRoutesDeps, createTokensRoutes };
25
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,87 @@
1
+ import { createScopeMiddleware } from "../middleware/scope.js";
2
+ import { createToken, listTokens, revokeToken } from "@graphorin/security";
3
+ import { Hono } from "hono";
4
+ import { z } from "zod";
5
+
6
+ //#region src/routes/tokens.ts
7
+ const CreateBodySchema = z.object({
8
+ label: z.string().min(1).optional(),
9
+ scopes: z.array(z.string().min(3)).min(1),
10
+ env: z.string().min(1).optional(),
11
+ expiresInMs: z.number().int().positive().optional()
12
+ }).strict();
13
+ /**
14
+ * @stable
15
+ */
16
+ function createTokensRoutes(deps) {
17
+ const app = new Hono();
18
+ app.get("/", createScopeMiddleware("tokens:list"), async (c) => {
19
+ const sanitized = (await listTokens(deps.tokenStore)).map((r) => ({
20
+ id: r.id,
21
+ label: r.label,
22
+ scopes: r.scopes,
23
+ createdAt: r.createdAt,
24
+ lastUsedAt: r.lastUsedAt,
25
+ expiresAt: r.expiresAt,
26
+ revokedAt: r.revokedAt
27
+ }));
28
+ return c.json({ tokens: sanitized });
29
+ });
30
+ app.post("/", createScopeMiddleware("tokens:create"), async (c) => {
31
+ const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));
32
+ if (!parsed.success) return c.json({
33
+ error: "config-invalid",
34
+ message: "Invalid create-token body.",
35
+ issues: parsed.error.issues.map((i) => ({
36
+ path: i.path,
37
+ message: i.message
38
+ }))
39
+ }, 400);
40
+ const env = parsed.data.env ?? deps.defaultEnv;
41
+ if (!deps.allowedEnvs.includes(env)) return c.json({
42
+ error: "config-invalid",
43
+ message: `Environment '${env}' is not in the allowed set.`,
44
+ allowed: deps.allowedEnvs
45
+ }, 400);
46
+ const created = await createToken({
47
+ tokenStore: deps.tokenStore,
48
+ pepper: deps.pepper,
49
+ env,
50
+ scopes: parsed.data.scopes,
51
+ ...parsed.data.label !== void 0 ? { label: parsed.data.label } : {},
52
+ ...parsed.data.expiresInMs !== void 0 ? { expiresInMs: parsed.data.expiresInMs } : {}
53
+ });
54
+ const raw = await created.raw.use((value) => value);
55
+ return c.json({
56
+ token: {
57
+ id: created.record.id,
58
+ label: created.record.label,
59
+ scopes: created.record.scopes,
60
+ createdAt: created.record.createdAt,
61
+ expiresAt: created.record.expiresAt
62
+ },
63
+ raw,
64
+ warning: "Store this raw value securely; it is shown exactly once."
65
+ }, 201);
66
+ });
67
+ app.delete("/:id", createScopeMiddleware("tokens:revoke"), async (c) => {
68
+ const id = c.req.param("id");
69
+ if (await revokeToken(deps.tokenStore, id) === void 0) return c.json({
70
+ error: "token-not-found",
71
+ message: `Token '${id}' not found.`
72
+ }, 404);
73
+ return c.body(null, 204);
74
+ });
75
+ return app;
76
+ }
77
+ async function safelyParseJson(c) {
78
+ try {
79
+ return await c.req.json();
80
+ } catch {
81
+ return {};
82
+ }
83
+ }
84
+
85
+ //#endregion
86
+ export { createTokensRoutes };
87
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +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"}
@@ -0,0 +1,27 @@
1
+ import { ServerVariables } from "../internal/context.js";
2
+ import { WorkflowRegistry } from "../registry/index.js";
3
+ import { WsDispatcher } from "../ws/dispatcher.js";
4
+ import { RunStateTracker } from "../runtime/run-state.js";
5
+ import { Hono } from "hono";
6
+
7
+ //#region src/routes/workflows.d.ts
8
+
9
+ /**
10
+ * @stable
11
+ */
12
+ interface WorkflowRoutesDeps {
13
+ readonly workflows: WorkflowRegistry;
14
+ readonly runs: RunStateTracker;
15
+ readonly newRunId?: () => string;
16
+ /** Streaming dispatcher (IP-2): workflow events reach the run subject. */
17
+ readonly dispatcher?: WsDispatcher;
18
+ }
19
+ /**
20
+ * @stable
21
+ */
22
+ declare function createWorkflowRoutes(deps: WorkflowRoutesDeps): Hono<{
23
+ Variables: ServerVariables;
24
+ }>;
25
+ //#endregion
26
+ export { WorkflowRoutesDeps, createWorkflowRoutes };
27
+ //# sourceMappingURL=workflows.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,220 @@
1
+ import { WorkflowNotFoundError } from "../errors/index.js";
2
+ import { createScopeMiddleware } from "../middleware/scope.js";
3
+ import { newRequestId } from "../internal/ids.js";
4
+ import { Hono } from "hono";
5
+ import { z } from "zod";
6
+
7
+ //#region src/routes/workflows.ts
8
+ const ExecuteBodySchema = z.object({
9
+ input: z.unknown().optional(),
10
+ threadId: z.string().min(1).optional(),
11
+ sessionId: z.string().min(1).optional(),
12
+ userId: z.string().min(1).optional()
13
+ }).strict().default({});
14
+ const ResumeBodySchema = z.object({
15
+ threadId: z.string().min(1),
16
+ resume: z.unknown().optional()
17
+ }).strict();
18
+ const ForkBodySchema = z.object({
19
+ fromThreadId: z.string().min(1),
20
+ fromCheckpointId: z.string().min(1).optional()
21
+ }).strict();
22
+ /**
23
+ * @stable
24
+ */
25
+ function createWorkflowRoutes(deps) {
26
+ const app = new Hono();
27
+ const newRunId = deps.newRunId ?? newRequestId;
28
+ app.get("/", createScopeMiddleware("workflows:read"), (c) => c.json({ workflows: deps.workflows.list() }));
29
+ app.post("/:id/execute", createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`), async (c) => {
30
+ const id = c.req.param("id");
31
+ const workflow = deps.workflows.get(id);
32
+ if (workflow === void 0) {
33
+ const err = new WorkflowNotFoundError(id);
34
+ return c.json({
35
+ error: err.kind,
36
+ message: err.message
37
+ }, 404);
38
+ }
39
+ const parsed = ExecuteBodySchema.safeParse(await safelyParseJson(c));
40
+ if (!parsed.success) return c.json({
41
+ error: "config-invalid",
42
+ message: "Invalid execute body.",
43
+ issues: parsed.error.issues.map((i) => ({
44
+ path: i.path,
45
+ message: i.message
46
+ }))
47
+ }, 400);
48
+ const runId = newRunId();
49
+ const tracker = deps.runs.start(runId, {
50
+ kind: "workflow",
51
+ workflowId: id,
52
+ ...parsed.data.threadId !== void 0 ? { threadId: parsed.data.threadId } : {},
53
+ ...parsed.data.sessionId !== void 0 ? { sessionId: parsed.data.sessionId } : {},
54
+ ...parsed.data.userId !== void 0 ? { userId: parsed.data.userId } : {}
55
+ });
56
+ const subject = `workflow:${id}/runs/${runId}/events`;
57
+ backgroundExecute(workflow, parsed.data, tracker, deps.runs, runId, {
58
+ subject,
59
+ ...deps.dispatcher !== void 0 ? { dispatcher: deps.dispatcher } : {}
60
+ });
61
+ return c.json({
62
+ runId,
63
+ status: "running",
64
+ subscribe: { websocket: subject }
65
+ }, 202);
66
+ });
67
+ app.post("/:id/resume", createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`), async (c) => {
68
+ const id = c.req.param("id");
69
+ const workflow = deps.workflows.get(id);
70
+ if (workflow === void 0) {
71
+ const err = new WorkflowNotFoundError(id);
72
+ return c.json({
73
+ error: err.kind,
74
+ message: err.message
75
+ }, 404);
76
+ }
77
+ const parsed = ResumeBodySchema.safeParse(await safelyParseJson(c));
78
+ if (!parsed.success) return c.json({
79
+ error: "config-invalid",
80
+ message: "Invalid resume body.",
81
+ issues: parsed.error.issues.map((i) => ({
82
+ path: i.path,
83
+ message: i.message
84
+ }))
85
+ }, 400);
86
+ if (workflow.resume === void 0) return c.json({
87
+ error: "workflow-resume-unsupported",
88
+ message: `Workflow '${id}' does not implement resume().`
89
+ }, 400);
90
+ const runId = newRunId();
91
+ deps.runs.declare(runId, {
92
+ kind: "workflow",
93
+ workflowId: id,
94
+ threadId: parsed.data.threadId
95
+ });
96
+ return c.json({
97
+ runId,
98
+ threadId: parsed.data.threadId,
99
+ status: "pending",
100
+ subscribe: {
101
+ websocket: `workflow:${id}/runs/${runId}/events`,
102
+ sse: `/v1/runs/${runId}/events`
103
+ }
104
+ }, 202);
105
+ });
106
+ app.get("/:id/state", createScopeMiddleware((_path, params) => `workflows:read:${params.id}`), async (c) => {
107
+ const id = c.req.param("id");
108
+ const workflow = deps.workflows.get(id);
109
+ if (workflow === void 0) {
110
+ const err = new WorkflowNotFoundError(id);
111
+ return c.json({
112
+ error: err.kind,
113
+ message: err.message
114
+ }, 404);
115
+ }
116
+ const threadId = c.req.query("threadId");
117
+ if (threadId === void 0 || threadId.length === 0) return c.json({
118
+ error: "config-invalid",
119
+ message: "Query parameter `threadId` is required."
120
+ }, 400);
121
+ if (workflow.getState === void 0) return c.json({
122
+ error: "workflow-state-unsupported",
123
+ message: "Workflow does not implement getState()."
124
+ }, 400);
125
+ const state = await workflow.getState(threadId);
126
+ return c.json({
127
+ workflowId: id,
128
+ threadId,
129
+ state
130
+ });
131
+ });
132
+ app.get("/:id/checkpoints", createScopeMiddleware((_path, params) => `workflows:read:${params.id}`), async (c) => {
133
+ const id = c.req.param("id");
134
+ const workflow = deps.workflows.get(id);
135
+ if (workflow === void 0) {
136
+ const err = new WorkflowNotFoundError(id);
137
+ return c.json({
138
+ error: err.kind,
139
+ message: err.message
140
+ }, 404);
141
+ }
142
+ const threadId = c.req.query("threadId");
143
+ if (threadId === void 0 || threadId.length === 0) return c.json({
144
+ error: "config-invalid",
145
+ message: "Query parameter `threadId` is required."
146
+ }, 400);
147
+ if (workflow.listCheckpoints === void 0) return c.json({
148
+ error: "workflow-checkpoints-unsupported",
149
+ message: "Workflow does not implement listCheckpoints()."
150
+ }, 400);
151
+ const checkpoints = await workflow.listCheckpoints(threadId);
152
+ return c.json({
153
+ workflowId: id,
154
+ threadId,
155
+ checkpoints
156
+ });
157
+ });
158
+ app.post("/:id/fork", createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`), async (c) => {
159
+ const id = c.req.param("id");
160
+ if (deps.workflows.get(id) === 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 = ForkBodySchema.safeParse(await safelyParseJson(c));
168
+ if (!parsed.success) return c.json({
169
+ error: "config-invalid",
170
+ message: "Invalid fork body.",
171
+ issues: parsed.error.issues.map((i) => ({
172
+ path: i.path,
173
+ message: i.message
174
+ }))
175
+ }, 400);
176
+ return c.json({
177
+ workflowId: id,
178
+ status: "pending",
179
+ fork: parsed.data
180
+ }, 202);
181
+ });
182
+ return app;
183
+ }
184
+ function backgroundExecute(workflow, body, tracker, runs, runId, streaming) {
185
+ const opts = { signal: tracker.signal };
186
+ if (body.threadId !== void 0) opts.threadId = body.threadId;
187
+ (async () => {
188
+ try {
189
+ for await (const ev of workflow.execute(body.input ?? {}, opts)) {
190
+ if (tracker.signal.aborted) break;
191
+ const type = typeof ev.type === "string" ? ev.type : "workflow.event";
192
+ streaming.dispatcher?.emit(streaming.subject, {
193
+ type,
194
+ payload: ev
195
+ });
196
+ }
197
+ runs.complete(runId, tracker.signal.aborted ? "aborted" : "completed");
198
+ } catch (err) {
199
+ streaming.dispatcher?.emit(streaming.subject, {
200
+ type: "workflow.error",
201
+ payload: {
202
+ runId,
203
+ message: err instanceof Error ? err.message : String(err)
204
+ }
205
+ });
206
+ runs.complete(runId, tracker.signal.aborted ? "aborted" : "failed", err);
207
+ }
208
+ })();
209
+ }
210
+ async function safelyParseJson(c) {
211
+ try {
212
+ return await c.req.json();
213
+ } catch {
214
+ return {};
215
+ }
216
+ }
217
+
218
+ //#endregion
219
+ export { createWorkflowRoutes };
220
+ //# sourceMappingURL=workflows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflows.js","names":["opts: { signal?: AbortSignal; threadId?: string }"],"sources":["../../src/routes/workflows.ts"],"sourcesContent":["/**\n * Workflow REST routes.\n *\n * POST /workflows/:id/execute (idempotent; scope `workflows:execute`)\n * POST /workflows/:id/resume (scope `workflows:resume[:id]`)\n * GET /workflows/:id/state (scope `workflows:read`)\n * GET /workflows/:id/checkpoints (scope `workflows:read`)\n * POST /workflows/:id/fork (scope `workflows:execute`)\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\n\nimport { WorkflowNotFoundError } from '../errors/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WorkflowRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface WorkflowRoutesDeps {\n readonly workflows: WorkflowRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /** Streaming dispatcher (IP-2): workflow events reach the run subject. */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst ExecuteBodySchema = z\n .object({\n input: z.unknown().optional(),\n threadId: z.string().min(1).optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\nconst ResumeBodySchema = z\n .object({\n threadId: z.string().min(1),\n resume: z.unknown().optional(),\n })\n .strict();\n\nconst ForkBodySchema = z\n .object({\n fromThreadId: z.string().min(1),\n fromCheckpointId: z.string().min(1).optional(),\n })\n .strict();\n\n/**\n * @stable\n */\nexport function createWorkflowRoutes(\n deps: WorkflowRoutesDeps,\n): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('workflows:read'), (c) =>\n c.json({ workflows: deps.workflows.list() }),\n );\n\n app.post(\n '/:id/execute',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ExecuteBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid execute body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const runId = newRunId();\n const tracker = deps.runs.start(runId, {\n kind: 'workflow',\n workflowId: id,\n ...(parsed.data.threadId !== undefined ? { threadId: parsed.data.threadId } : {}),\n ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),\n ...(parsed.data.userId !== undefined ? { userId: parsed.data.userId } : {}),\n });\n // Workflows are streaming-first; the REST endpoint kicks off the\n // run in the background and returns 202 + runId. The actual\n // event stream is consumed via WebSocket / SSE in Phase 14b.\n const subject = `workflow:${id}/runs/${runId}/events`;\n backgroundExecute(workflow, parsed.data, tracker, deps.runs, runId, {\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // IP-2: the previously-advertised SSE URL was never\n // mounted; the WS subject (now in the grammar) delivers.\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n app.post(\n '/:id/resume',\n createScopeMiddleware((_path, params) => `workflows:resume:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ResumeBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid resume body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n if (workflow.resume === undefined) {\n return c.json(\n {\n error: 'workflow-resume-unsupported',\n message: `Workflow '${id}' does not implement resume().`,\n },\n 400,\n );\n }\n const runId = newRunId();\n deps.runs.declare(runId, {\n kind: 'workflow',\n workflowId: id,\n threadId: parsed.data.threadId,\n });\n return c.json(\n {\n runId,\n threadId: parsed.data.threadId,\n status: 'pending',\n subscribe: {\n websocket: `workflow:${id}/runs/${runId}/events`,\n sse: `/v1/runs/${runId}/events`,\n },\n },\n 202,\n );\n },\n );\n\n app.get(\n '/:id/state',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.getState === undefined) {\n return c.json(\n {\n error: 'workflow-state-unsupported',\n message: 'Workflow does not implement getState().',\n },\n 400,\n );\n }\n const state = await workflow.getState(threadId);\n return c.json({ workflowId: id, threadId, state });\n },\n );\n\n app.get(\n '/:id/checkpoints',\n createScopeMiddleware((_path, params) => `workflows:read:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const threadId = c.req.query('threadId');\n if (threadId === undefined || threadId.length === 0) {\n return c.json(\n { error: 'config-invalid', message: 'Query parameter `threadId` is required.' },\n 400,\n );\n }\n if (workflow.listCheckpoints === undefined) {\n return c.json(\n {\n error: 'workflow-checkpoints-unsupported',\n message: 'Workflow does not implement listCheckpoints().',\n },\n 400,\n );\n }\n const checkpoints = await workflow.listCheckpoints(threadId);\n return c.json({ workflowId: id, threadId, checkpoints });\n },\n );\n\n app.post(\n '/:id/fork',\n createScopeMiddleware((_path, params) => `workflows:execute:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const workflow = deps.workflows.get(id);\n if (workflow === undefined) {\n const err = new WorkflowNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = ForkBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid fork body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n // Fork delegates to the workflow's own primitives in Phase 14b/c.\n // Phase 14a returns a structured 202 with the fork descriptor so\n // the API contract is reachable today.\n return c.json(\n {\n workflowId: id,\n status: 'pending',\n fork: parsed.data,\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\nfunction backgroundExecute(\n workflow: NonNullable<ReturnType<WorkflowRegistry['get']>>,\n body: z.infer<typeof ExecuteBodySchema>,\n tracker: { readonly signal: AbortSignal },\n runs: RunStateTracker,\n runId: string,\n streaming: {\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const opts: { signal?: AbortSignal; threadId?: string } = { signal: tracker.signal };\n if (body.threadId !== undefined) opts.threadId = body.threadId;\n void (async () => {\n try {\n // IP-2: every workflow event reaches the dispatcher — the old\n // loop iterated the stream and threw each event away.\n for await (const ev of workflow.execute(body.input ?? {}, opts)) {\n if (tracker.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'workflow.event';\n streaming.dispatcher?.emit(streaming.subject, { type, payload: ev });\n }\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n streaming.dispatcher?.emit(streaming.subject, {\n type: 'workflow.error',\n payload: { runId, message: err instanceof Error ? err.message : String(err) },\n });\n runs.complete(runId, tracker.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ //#region src/runtime/run-state.d.ts
2
+ /**
3
+ * In-memory bookkeeping for in-flight agent / workflow runs. Exposes
4
+ * a tiny CRUD surface route handlers consume to honour the
5
+ * `GET /runs/:runId/state` and `POST /runs/:runId/abort` endpoints
6
+ * declared in the runtime architecture.
7
+ *
8
+ * The full durable resume / replay path lives in Phase 14b/c on top
9
+ * of the WebSocket layer + the consolidator daemon. Phase 14a only
10
+ * needs enough state to:
11
+ * - mint a runId on every `POST /run` / `POST /stream`,
12
+ * - track its lifecycle (`pending` → `running` → `completed` / `failed` / `aborted`),
13
+ * - propagate `AbortController.signal` so handlers can cancel.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ /**
18
+ * Stable status discriminator for a run snapshot. Mirrors the values
19
+ * exposed on the public REST surface.
20
+ *
21
+ * @stable
22
+ */
23
+ type RunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'aborted';
24
+ /**
25
+ * Identifying tag for the underlying execution kind. Workflows run
26
+ * on the durable engine in `@graphorin/workflow`; agents run on the
27
+ * `@graphorin/agent` runtime.
28
+ *
29
+ * @stable
30
+ */
31
+ type RunKind = 'agent' | 'workflow';
32
+ /**
33
+ * Terminal status a run can settle into (never `pending` / `running`).
34
+ *
35
+ * @stable
36
+ */
37
+ type TerminalRunStatus = Extract<RunStatus, 'completed' | 'failed' | 'aborted'>;
38
+ /**
39
+ * IP-15: payload handed to the {@link RunStateTracker} `onTerminal` callback
40
+ * the first time a run reaches a terminal state. The server turns this into
41
+ * the `graphorin_agent_runs_total` counter + `graphorin_agent_run_duration_seconds`
42
+ * summary. `durationMs` is omitted for a run that was aborted before it ever
43
+ * started (no meaningful wall-clock).
44
+ *
45
+ * @stable
46
+ */
47
+ interface TerminalRunInfo {
48
+ readonly status: TerminalRunStatus;
49
+ readonly kind: RunKind;
50
+ readonly durationMs?: number;
51
+ }
52
+ /**
53
+ * Snapshot returned by {@link RunStateTracker.snapshot}.
54
+ *
55
+ * @stable
56
+ */
57
+ interface RunStateSnapshot {
58
+ readonly runId: string;
59
+ readonly kind: RunKind;
60
+ readonly status: RunStatus;
61
+ readonly startedAt?: number;
62
+ readonly completedAt?: number;
63
+ readonly error?: {
64
+ readonly message: string;
65
+ };
66
+ readonly agentId?: string;
67
+ readonly workflowId?: string;
68
+ readonly threadId?: string;
69
+ readonly sessionId?: string;
70
+ readonly userId?: string;
71
+ }
72
+ /**
73
+ * Bookkeeping descriptor recorded at run start. Either an agent run
74
+ * (with `agentId`) or a workflow run (with `workflowId` + optional
75
+ * `threadId`).
76
+ *
77
+ * @stable
78
+ */
79
+ type RunDescriptor = {
80
+ readonly kind: 'agent';
81
+ readonly agentId: string;
82
+ readonly sessionId?: string;
83
+ readonly userId?: string;
84
+ } | {
85
+ readonly kind: 'workflow';
86
+ readonly workflowId: string;
87
+ readonly threadId?: string;
88
+ readonly sessionId?: string;
89
+ readonly userId?: string;
90
+ };
91
+ /**
92
+ * In-flight handle returned by {@link RunStateTracker.start}. Handlers
93
+ * pass `signal` into the underlying `agent.run / workflow.execute`
94
+ * invocation so cancellation propagates instantly.
95
+ *
96
+ * @stable
97
+ */
98
+ interface RunHandle {
99
+ readonly runId: string;
100
+ readonly signal: AbortSignal;
101
+ cancel(reason?: unknown): void;
102
+ }
103
+ /**
104
+ * Pluggable tracker. The default in-memory implementation is the only
105
+ * one shipped in Phase 14a; future phases plug in a SQLite-backed
106
+ * variant so durable resume survives process restarts.
107
+ *
108
+ * @stable
109
+ */
110
+ declare class RunStateTracker {
111
+ #private;
112
+ constructor(options?: {
113
+ readonly now?: () => number;
114
+ /**
115
+ * IP-15: invoked exactly once per run, the first time it settles into a
116
+ * terminal state. Used to drive the run-count + duration metrics. Never
117
+ * throws into the tracker — wrap your handler if it might.
118
+ */
119
+ readonly onTerminal?: (info: TerminalRunInfo) => void;
120
+ });
121
+ /** Reserve a run id without taking ownership of an AbortSignal. */
122
+ declare(runId: string, descriptor: RunDescriptor): void;
123
+ /** Promote a previously-declared run to `running` (or declare it). */
124
+ start(runId: string, descriptor: RunDescriptor): RunHandle;
125
+ /** Mark a run as terminal. */
126
+ complete(runId: string, status: Extract<RunStatus, 'completed' | 'failed' | 'aborted'>, err?: unknown): void;
127
+ /** Cancel a run via its `AbortController`. */
128
+ abort(runId: string, reason?: unknown): boolean;
129
+ /** Read-only snapshot, safe to JSON.stringify. */
130
+ snapshot(runId: string): RunStateSnapshot | undefined;
131
+ /**
132
+ * Number of runs currently in `pending` or `running`. Useful for
133
+ * snapshots / metrics. Note that `pending` runs hold a reservation
134
+ * but have not yet started any work — see {@link runningCount} for
135
+ * the drain-blocking subset.
136
+ */
137
+ inflightCount(): number;
138
+ /**
139
+ * Number of runs with active work in progress (`running`). The
140
+ * lifecycle drain blocks on this counter only — pending runs are a
141
+ * pure reservation (e.g. an awaited WS subscription) and can be
142
+ * aborted immediately when SIGTERM arrives.
143
+ */
144
+ runningCount(): number;
145
+ /**
146
+ * Drop every reserved-but-not-yet-started run. Called by the
147
+ * server lifecycle at the start of `stop()` so the drain only
148
+ * waits for actual work in flight.
149
+ */
150
+ abortPending(reason?: unknown): number;
151
+ /** Drop terminal records older than `olderThan`. */
152
+ prune(olderThan: number): number;
153
+ /** Cancel every in-flight run. Used during graceful shutdown. */
154
+ abortAll(reason?: unknown): number;
155
+ }
156
+ //#endregion
157
+ export { RunDescriptor, RunHandle, RunStateSnapshot, RunStateTracker, RunStatus };
158
+ //# sourceMappingURL=run-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-state.d.ts","names":[],"sources":["../../src/runtime/run-state.ts"],"sourcesContent":[],"mappings":";;AAsBA;AASA;AAOA;AAWA;AAeA;AAqBA;AAsBA;AAwBA;;;;;;;;;;;;;KA7GY,SAAA;;;;;;;;KASA,OAAA;;;;;;KAOA,iBAAA,GAAoB,QAAQ;;;;;;;;;;UAWvB,eAAA;mBACE;iBACF;;;;;;;;UAaA,gBAAA;;iBAEA;mBACE;;;;;;;;;;;;;;;;;;;KAkBP,aAAA;;;;;;;;;;;;;;;;;;;UAsBK,SAAA;;mBAEE;;;;;;;;;;cAsBN,eAAA;;;;;;;;;iCAasB;;;qCAsBE;;mCAaF,gBAAgB;;kCA0BvC,QAAQ;;;;2BAgCO"}