@graphorin/server 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +6 -6
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +40 -611
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +9 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +22 -26
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +6 -0
  59. package/dist/package.js.map +1 -0
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.d.ts.map +1 -1
  99. package/dist/ws/dispatcher.js +19 -12
  100. package/dist/ws/dispatcher.js.map +1 -1
  101. package/dist/ws/index.d.ts +2 -2
  102. package/dist/ws/index.js +3 -3
  103. package/dist/ws/replay-buffer.d.ts +35 -1
  104. package/dist/ws/replay-buffer.d.ts.map +1 -1
  105. package/dist/ws/replay-buffer.js +35 -3
  106. package/dist/ws/replay-buffer.js.map +1 -1
  107. package/dist/ws/upgrade.d.ts.map +1 -1
  108. package/dist/ws/upgrade.js +26 -19
  109. package/dist/ws/upgrade.js.map +1 -1
  110. package/package.json +31 -30
  111. package/src/app-audit-binding.ts +227 -0
  112. package/src/app-daemons.ts +73 -0
  113. package/src/app-lifecycle.ts +476 -0
  114. package/src/app-metrics.ts +144 -0
  115. package/src/app-middleware.ts +149 -0
  116. package/src/app-routes.ts +305 -0
  117. package/src/app-ws.ts +111 -0
  118. package/src/app.ts +317 -0
  119. package/src/commentary/audit-bridge.ts +135 -0
  120. package/src/commentary/built-in-patterns.ts +79 -0
  121. package/src/commentary/index.ts +32 -0
  122. package/src/commentary/sanitizer.ts +238 -0
  123. package/src/commentary/types.ts +130 -0
  124. package/src/config.ts +472 -0
  125. package/src/consolidator/daemon.ts +141 -0
  126. package/src/consolidator/index.ts +14 -0
  127. package/src/errors/index.ts +247 -0
  128. package/src/health/checks.ts +322 -0
  129. package/src/health/index.ts +34 -0
  130. package/src/health/routes.ts +154 -0
  131. package/src/index.ts +243 -0
  132. package/src/internal/context.ts +77 -0
  133. package/src/internal/ids.ts +17 -0
  134. package/src/internal/json.ts +47 -0
  135. package/src/internal/wire-error.ts +44 -0
  136. package/src/lifecycle/hooks.ts +66 -0
  137. package/src/lifecycle/index.ts +16 -0
  138. package/src/lifecycle/pre-bind.ts +138 -0
  139. package/src/metrics/catalog.ts +112 -0
  140. package/src/metrics/index.ts +12 -0
  141. package/src/metrics/registry.ts +337 -0
  142. package/src/metrics/tools-bridge.ts +124 -0
  143. package/src/middleware/audit.ts +114 -0
  144. package/src/middleware/auth.ts +195 -0
  145. package/src/middleware/cors.ts +72 -0
  146. package/src/middleware/csrf.ts +98 -0
  147. package/src/middleware/idempotency.ts +336 -0
  148. package/src/middleware/index.ts +38 -0
  149. package/src/middleware/rate-limit.ts +71 -0
  150. package/src/middleware/request-state.ts +79 -0
  151. package/src/middleware/scope.ts +161 -0
  152. package/src/registry/index.ts +278 -0
  153. package/src/replay/index.ts +14 -0
  154. package/src/replay/routes.ts +255 -0
  155. package/src/routes/agents.ts +363 -0
  156. package/src/routes/audit.ts +207 -0
  157. package/src/routes/auth.ts +80 -0
  158. package/src/routes/health.ts +42 -0
  159. package/src/routes/index.ts +16 -0
  160. package/src/routes/mcp.ts +97 -0
  161. package/src/routes/memory.ts +152 -0
  162. package/src/routes/sessions.ts +250 -0
  163. package/src/routes/skills.ts +91 -0
  164. package/src/routes/tokens.ts +166 -0
  165. package/src/routes/workflows.ts +616 -0
  166. package/src/runtime/retention.ts +284 -0
  167. package/src/runtime/run-state.ts +422 -0
  168. package/src/sse/events.ts +303 -0
  169. package/src/sse/index.ts +7 -0
  170. package/src/tools-audit-bridge.ts +120 -0
  171. package/src/triggers/daemon.ts +198 -0
  172. package/src/triggers/index.ts +16 -0
  173. package/src/triggers/routes.ts +139 -0
  174. package/src/workflows/timer-daemon.ts +102 -0
  175. package/src/ws/dispatcher.ts +537 -0
  176. package/src/ws/index.ts +45 -0
  177. package/src/ws/replay-buffer.ts +209 -0
  178. package/src/ws/subjects.ts +162 -0
  179. package/src/ws/ticket.ts +174 -0
  180. package/src/ws/upgrade.ts +507 -0
  181. package/dist/lifecycle/index.js +0 -3
  182. package/dist/routes/index.js +0 -12
@@ -0,0 +1,16 @@
1
+ /**
2
+ * REST route barrel.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export { type AgentRoutesDeps, createAgentRoutes, createRunRoutes } from './agents.js';
8
+ export { type AuditApi, type AuditRoutesDeps, createAuditRoutes } from './audit.js';
9
+ export { type AuthRoutesDeps, createAuthRoutes } from './auth.js';
10
+ export { createHealthRoutes, type HealthRoutesDeps } from './health.js';
11
+ export { createMcpRoutes, type McpApi, type McpRoutesDeps } from './mcp.js';
12
+ export { createMemoryRoutes, type MemoryApi, type MemoryRoutesDeps } from './memory.js';
13
+ export { createSessionRoutes, type SessionApi, type SessionRoutesDeps } from './sessions.js';
14
+ export { createSkillsRoutes, type SkillsApi, type SkillsRoutesDeps } from './skills.js';
15
+ export { createTokensRoutes, type TokensRoutesDeps } from './tokens.js';
16
+ export { createWorkflowRoutes, type WorkflowRoutesDeps } from './workflows.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * MCP server-management REST routes.
3
+ *
4
+ * GET /mcp/servers (scope `mcp:admin`)
5
+ * POST /mcp/servers (idempotent; scope `mcp:admin`)
6
+ * DELETE /mcp/servers/:id (scope `mcp:admin`)
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type { Context } from 'hono';
12
+ import { Hono } from 'hono';
13
+ import { z } from 'zod';
14
+
15
+ import type { ServerVariables } from '../internal/context.js';
16
+ import { createScopeMiddleware } from '../middleware/scope.js';
17
+
18
+ /**
19
+ * @stable
20
+ */
21
+ export interface McpApi {
22
+ list(): Promise<ReadonlyArray<unknown>>;
23
+ register(input: {
24
+ readonly id: string;
25
+ readonly transport: string;
26
+ readonly url?: string;
27
+ }): Promise<unknown>;
28
+ remove(id: string): Promise<boolean>;
29
+ }
30
+
31
+ const RegisterBodySchema = z
32
+ .object({
33
+ id: z.string().min(1),
34
+ transport: z.enum(['stdio', 'streamable-http', 'sse']),
35
+ url: z.string().min(1).optional(),
36
+ })
37
+ .strict();
38
+
39
+ /**
40
+ * @stable
41
+ */
42
+ export interface McpRoutesDeps {
43
+ readonly mcp: McpApi;
44
+ }
45
+
46
+ /**
47
+ * @stable
48
+ */
49
+ export function createMcpRoutes(deps: McpRoutesDeps): Hono<{ Variables: ServerVariables }> {
50
+ const app = new Hono<{ Variables: ServerVariables }>();
51
+
52
+ app.get('/servers', createScopeMiddleware('mcp:admin'), async (c) => {
53
+ return c.json({ servers: await deps.mcp.list() });
54
+ });
55
+
56
+ app.post('/servers', createScopeMiddleware('mcp:admin'), async (c) => {
57
+ const parsed = RegisterBodySchema.safeParse(await safelyParseJson(c));
58
+ if (!parsed.success) {
59
+ return c.json(
60
+ {
61
+ error: 'config-invalid',
62
+ message: 'Invalid MCP server body.',
63
+ issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
64
+ },
65
+ 400,
66
+ );
67
+ }
68
+ const payload: { id: string; transport: string; url?: string } = {
69
+ id: parsed.data.id,
70
+ transport: parsed.data.transport,
71
+ };
72
+ if (parsed.data.url !== undefined) payload.url = parsed.data.url;
73
+ const out = await deps.mcp.register(payload);
74
+ return c.json({ server: out }, 201);
75
+ });
76
+
77
+ app.delete('/servers/:id', createScopeMiddleware('mcp:admin'), async (c) => {
78
+ const id = c.req.param('id');
79
+ const removed = await deps.mcp.remove(id);
80
+ if (!removed)
81
+ return c.json(
82
+ { error: 'mcp-server-not-found', message: `MCP server '${id}' not found.` },
83
+ 404,
84
+ );
85
+ return c.body(null, 204);
86
+ });
87
+
88
+ return app;
89
+ }
90
+
91
+ async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
92
+ try {
93
+ return await c.req.json();
94
+ } catch {
95
+ return {};
96
+ }
97
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Memory REST routes. The handlers delegate to a structurally-typed
3
+ * `MemoryApi` provided by the operator (in practice, a thin shim
4
+ * around `@graphorin/memory.createMemory({...})`).
5
+ *
6
+ * POST /memory/search (scope `memory:read`)
7
+ * POST /memory/facts (idempotent; scope `memory:write`)
8
+ * DELETE /memory/facts/:id (scope `memory:write`)
9
+ * POST /memory/blocks (idempotent; scope `memory:write`)
10
+ * DELETE /memory/blocks/:label (scope `memory:write`)
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import type { Context } from 'hono';
16
+ import { Hono } from 'hono';
17
+ import { z } from 'zod';
18
+
19
+ import type { ServerVariables } from '../internal/context.js';
20
+ import { createScopeMiddleware } from '../middleware/scope.js';
21
+
22
+ const ScopeSchema = z
23
+ .object({
24
+ userId: z.string().min(1),
25
+ agentId: z.string().min(1).optional(),
26
+ sessionId: z.string().min(1).optional(),
27
+ })
28
+ .strict();
29
+
30
+ const SearchBodySchema = z
31
+ .object({
32
+ scope: ScopeSchema,
33
+ query: z.string().min(1),
34
+ topK: z.number().int().positive().max(200).optional(),
35
+ })
36
+ .strict();
37
+
38
+ const RememberBodySchema = z
39
+ .object({
40
+ scope: ScopeSchema,
41
+ text: z.string().min(1),
42
+ sensitivity: z.enum(['public', 'internal', 'secret']).optional(),
43
+ metadata: z.record(z.string(), z.unknown()).optional(),
44
+ })
45
+ .strict();
46
+
47
+ const BlockBodySchema = z
48
+ .object({
49
+ scope: ScopeSchema,
50
+ label: z.string().min(1),
51
+ body: z.string(),
52
+ })
53
+ .strict();
54
+
55
+ /**
56
+ * @stable
57
+ */
58
+ export interface MemoryApi {
59
+ search(input: z.infer<typeof SearchBodySchema>): Promise<ReadonlyArray<unknown>>;
60
+ remember(input: z.infer<typeof RememberBodySchema>): Promise<{ readonly factId: string }>;
61
+ forget(scope: { readonly userId: string }, factId: string): Promise<boolean>;
62
+ upsertBlock(input: z.infer<typeof BlockBodySchema>): Promise<{ readonly label: string }>;
63
+ deleteBlock(scope: { readonly userId: string }, label: string): Promise<boolean>;
64
+ }
65
+
66
+ /**
67
+ * @stable
68
+ */
69
+ export interface MemoryRoutesDeps {
70
+ readonly memory: MemoryApi;
71
+ }
72
+
73
+ /**
74
+ * @stable
75
+ */
76
+ export function createMemoryRoutes(deps: MemoryRoutesDeps): Hono<{ Variables: ServerVariables }> {
77
+ const app = new Hono<{ Variables: ServerVariables }>();
78
+
79
+ app.post('/search', createScopeMiddleware('memory:read'), async (c) => {
80
+ const parsed = SearchBodySchema.safeParse(await safelyParseJson(c));
81
+ if (!parsed.success) {
82
+ return invalid(c, parsed.error.issues);
83
+ }
84
+ const hits = await deps.memory.search(parsed.data);
85
+ return c.json({ hits });
86
+ });
87
+
88
+ app.post('/facts', createScopeMiddleware('memory:write'), async (c) => {
89
+ const parsed = RememberBodySchema.safeParse(await safelyParseJson(c));
90
+ if (!parsed.success) return invalid(c, parsed.error.issues);
91
+ const out = await deps.memory.remember(parsed.data);
92
+ return c.json({ fact: out }, 201);
93
+ });
94
+
95
+ app.delete('/facts/:id', createScopeMiddleware('memory:write'), async (c) => {
96
+ const id = c.req.param('id');
97
+ const userId = c.req.query('userId');
98
+ if (userId === undefined || userId.length === 0) {
99
+ return c.json(
100
+ { error: 'config-invalid', message: 'Query parameter `userId` is required.' },
101
+ 400,
102
+ );
103
+ }
104
+ const removed = await deps.memory.forget({ userId }, id);
105
+ if (!removed)
106
+ return c.json({ error: 'fact-not-found', message: `Fact '${id}' not found.` }, 404);
107
+ return c.body(null, 204);
108
+ });
109
+
110
+ app.post('/blocks', createScopeMiddleware('memory:write'), async (c) => {
111
+ const parsed = BlockBodySchema.safeParse(await safelyParseJson(c));
112
+ if (!parsed.success) return invalid(c, parsed.error.issues);
113
+ const out = await deps.memory.upsertBlock(parsed.data);
114
+ return c.json({ block: out }, 201);
115
+ });
116
+
117
+ app.delete('/blocks/:label', createScopeMiddleware('memory:write'), async (c) => {
118
+ const label = c.req.param('label');
119
+ const userId = c.req.query('userId');
120
+ if (userId === undefined || userId.length === 0) {
121
+ return c.json(
122
+ { error: 'config-invalid', message: 'Query parameter `userId` is required.' },
123
+ 400,
124
+ );
125
+ }
126
+ const removed = await deps.memory.deleteBlock({ userId }, label);
127
+ if (!removed)
128
+ return c.json({ error: 'block-not-found', message: `Block '${label}' not found.` }, 404);
129
+ return c.body(null, 204);
130
+ });
131
+
132
+ return app;
133
+ }
134
+
135
+ function invalid(c: Context<{ Variables: ServerVariables }>, issues: ReadonlyArray<z.ZodIssue>) {
136
+ return c.json(
137
+ {
138
+ error: 'config-invalid',
139
+ message: 'Invalid request body.',
140
+ issues: issues.map((i) => ({ path: i.path, message: i.message })),
141
+ },
142
+ 400,
143
+ );
144
+ }
145
+
146
+ async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
147
+ try {
148
+ return await c.req.json();
149
+ } catch {
150
+ return {};
151
+ }
152
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Session REST routes. Delegates to a structurally-typed `SessionApi`
3
+ * supplied by the operator (in practice, the `@graphorin/sessions`
4
+ * package's `SessionManager`); the contract is intentionally open so
5
+ * tests can stub it without dragging the full sessions facade in.
6
+ *
7
+ * GET /sessions (scope `sessions:read`)
8
+ * GET /sessions/:id (scope `sessions:read`)
9
+ * POST /sessions (idempotent; scope `sessions:write`)
10
+ * DELETE /sessions/:id (scope `sessions:write`)
11
+ * GET /sessions/:id/messages (scope `sessions:read`)
12
+ * GET /sessions/:id/handoffs (scope `sessions:read`)
13
+ * POST /sessions/:id/export (scope `sessions:export`)
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ import type { Context } from 'hono';
19
+ import { Hono } from 'hono';
20
+ import { z } from 'zod';
21
+
22
+ import type { ServerVariables } from '../internal/context.js';
23
+ import { createScopeMiddleware } from '../middleware/scope.js';
24
+
25
+ /**
26
+ * Minimal contract route handlers consume. Real deployments wire
27
+ * `@graphorin/sessions.SessionManager` in directly; tests pass a
28
+ * lighter stub.
29
+ *
30
+ * @stable
31
+ */
32
+ export interface SessionApi {
33
+ list(opts: {
34
+ readonly userId?: string;
35
+ readonly agentId?: string;
36
+ }): Promise<ReadonlyArray<unknown>>;
37
+ get(sessionId: string): Promise<unknown | null>;
38
+ create(input: {
39
+ readonly userId: string;
40
+ readonly agentId: string;
41
+ readonly sessionId?: string;
42
+ readonly title?: string;
43
+ readonly tags?: ReadonlyArray<string>;
44
+ }): Promise<unknown>;
45
+ remove(sessionId: string): Promise<boolean>;
46
+ listMessages(
47
+ sessionId: string,
48
+ opts: { readonly limit?: number },
49
+ ): Promise<ReadonlyArray<unknown>>;
50
+ listHandoffs(sessionId: string): Promise<ReadonlyArray<unknown>>;
51
+ exportSession?(
52
+ sessionId: string,
53
+ opts: {
54
+ readonly includeAuditEntries?: boolean;
55
+ readonly hash?: boolean;
56
+ },
57
+ ): Promise<unknown>;
58
+ replaySession?(
59
+ sessionId: string,
60
+ opts: { readonly raw?: boolean; readonly fromMessageId?: string },
61
+ ): AsyncIterable<unknown>;
62
+ }
63
+
64
+ const CreateBodySchema = z
65
+ .object({
66
+ userId: z.string().min(1),
67
+ agentId: z.string().min(1),
68
+ sessionId: z.string().min(1).optional(),
69
+ title: z.string().min(1).optional(),
70
+ tags: z.array(z.string()).optional(),
71
+ })
72
+ .strict();
73
+
74
+ const MessagesQuerySchema = z
75
+ .object({
76
+ limit: z.coerce.number().int().positive().max(1000).optional(),
77
+ })
78
+ .strict();
79
+
80
+ const ExportBodySchema = z
81
+ .object({
82
+ includeAuditEntries: z.boolean().optional(),
83
+ hash: z.boolean().optional(),
84
+ })
85
+ .strict()
86
+ .default({});
87
+
88
+ const _ReplayBodySchema = z
89
+ .object({
90
+ raw: z.boolean().optional(),
91
+ fromMessageId: z.string().min(1).optional(),
92
+ })
93
+ .strict()
94
+ .default({});
95
+
96
+ /**
97
+ * @stable
98
+ */
99
+ export interface SessionRoutesDeps {
100
+ readonly sessions: SessionApi;
101
+ }
102
+
103
+ /**
104
+ * @stable
105
+ */
106
+ export function createSessionRoutes(deps: SessionRoutesDeps): Hono<{ Variables: ServerVariables }> {
107
+ const app = new Hono<{ Variables: ServerVariables }>();
108
+
109
+ app.get('/', createScopeMiddleware('sessions:read'), async (c) => {
110
+ const userId = c.req.query('userId');
111
+ const agentId = c.req.query('agentId');
112
+ const filter: { userId?: string; agentId?: string } = {};
113
+ if (userId !== undefined && userId.length > 0) filter.userId = userId;
114
+ if (agentId !== undefined && agentId.length > 0) filter.agentId = agentId;
115
+ const list = await deps.sessions.list(filter);
116
+ return c.json({ sessions: list });
117
+ });
118
+
119
+ app.post('/', createScopeMiddleware('sessions:write'), async (c) => {
120
+ const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));
121
+ if (!parsed.success) {
122
+ return c.json(
123
+ {
124
+ error: 'config-invalid',
125
+ message: 'Invalid session body.',
126
+ issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
127
+ },
128
+ 400,
129
+ );
130
+ }
131
+ const session = await deps.sessions.create({
132
+ userId: parsed.data.userId,
133
+ agentId: parsed.data.agentId,
134
+ ...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),
135
+ ...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),
136
+ ...(parsed.data.tags !== undefined ? { tags: parsed.data.tags } : {}),
137
+ });
138
+ return c.json({ session }, 201);
139
+ });
140
+
141
+ // W-107: per-resource requirement - a token scoped to ONE session
142
+ // reads it on every surface (WS subjects already gated per-resource;
143
+ // wide two-segment grants keep covering three-segment requirements).
144
+ app.get(
145
+ '/:id',
146
+ createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
147
+ async (c) => {
148
+ const id = c.req.param('id');
149
+ const session = await deps.sessions.get(id);
150
+ if (session === null) {
151
+ return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);
152
+ }
153
+ return c.json({ session });
154
+ },
155
+ );
156
+
157
+ app.delete(
158
+ '/:id',
159
+ createScopeMiddleware((_path, params) => `sessions:write:${params.id}`),
160
+ async (c) => {
161
+ const id = c.req.param('id');
162
+ const removed = await deps.sessions.remove(id);
163
+ if (!removed) {
164
+ return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);
165
+ }
166
+ return c.body(null, 204);
167
+ },
168
+ );
169
+
170
+ app.get(
171
+ '/:id/messages',
172
+ createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
173
+ async (c) => {
174
+ const id = c.req.param('id');
175
+ const parsed = MessagesQuerySchema.safeParse({ limit: c.req.query('limit') });
176
+ if (!parsed.success) {
177
+ return c.json(
178
+ {
179
+ error: 'config-invalid',
180
+ message: 'Invalid messages query.',
181
+ issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
182
+ },
183
+ 400,
184
+ );
185
+ }
186
+ const opts: { limit?: number } = {};
187
+ if (parsed.data.limit !== undefined) opts.limit = parsed.data.limit;
188
+ const messages = await deps.sessions.listMessages(id, opts);
189
+ return c.json({ sessionId: id, messages });
190
+ },
191
+ );
192
+
193
+ app.get(
194
+ '/:id/handoffs',
195
+ createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
196
+ async (c) => {
197
+ const id = c.req.param('id');
198
+ const handoffs = await deps.sessions.listHandoffs(id);
199
+ return c.json({ sessionId: id, handoffs });
200
+ },
201
+ );
202
+
203
+ app.post(
204
+ '/:id/export',
205
+ createScopeMiddleware((_path, params) => `sessions:export:${params.id}`),
206
+ async (c) => {
207
+ const id = c.req.param('id');
208
+ if (deps.sessions.exportSession === undefined) {
209
+ return c.json(
210
+ { error: 'session-export-unsupported', message: 'Export is not enabled.' },
211
+ 400,
212
+ );
213
+ }
214
+ const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));
215
+ if (!parsed.success) {
216
+ return c.json(
217
+ {
218
+ error: 'config-invalid',
219
+ message: 'Invalid export body.',
220
+ issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
221
+ },
222
+ 400,
223
+ );
224
+ }
225
+ const exportOpts: { includeAuditEntries?: boolean; hash?: boolean } = {};
226
+ if (parsed.data.includeAuditEntries !== undefined) {
227
+ exportOpts.includeAuditEntries = parsed.data.includeAuditEntries;
228
+ }
229
+ if (parsed.data.hash !== undefined) exportOpts.hash = parsed.data.hash;
230
+ const result = await deps.sessions.exportSession(id, exportOpts);
231
+ return c.json({ sessionId: id, export: result });
232
+ },
233
+ );
234
+
235
+ // IP-14: the session-replay route lives in `replay/routes.ts` (the
236
+ // scope-laddered, audit-backed implementation). The Phase-14a stub
237
+ // that lived here SHADOWED it (mounted earlier on the same path)
238
+ // while advertising a `session:<id>/replay` subject the WS grammar
239
+ // cannot parse - deleted.
240
+
241
+ return app;
242
+ }
243
+
244
+ async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
245
+ try {
246
+ return await c.req.json();
247
+ } catch {
248
+ return {};
249
+ }
250
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Skill REST routes.
3
+ *
4
+ * GET /skills (scope `skills:read`)
5
+ * POST /skills/install (idempotent; scope `skills:install`)
6
+ * GET /skills/:name (scope `skills:read`)
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type { Context } from 'hono';
12
+ import { Hono } from 'hono';
13
+ import { z } from 'zod';
14
+
15
+ import type { ServerVariables } from '../internal/context.js';
16
+ import { createScopeMiddleware } from '../middleware/scope.js';
17
+
18
+ /**
19
+ * @stable
20
+ */
21
+ export interface SkillsApi {
22
+ list(): Promise<ReadonlyArray<{ readonly name: string; readonly version?: string }>>;
23
+ get(name: string): Promise<unknown | null>;
24
+ install(input: {
25
+ readonly source: string;
26
+ readonly trust?: 'verified' | 'unverified';
27
+ }): Promise<unknown>;
28
+ }
29
+
30
+ const InstallBodySchema = z
31
+ .object({
32
+ source: z.string().min(1),
33
+ trust: z.enum(['verified', 'unverified']).optional(),
34
+ })
35
+ .strict();
36
+
37
+ /**
38
+ * @stable
39
+ */
40
+ export interface SkillsRoutesDeps {
41
+ readonly skills: SkillsApi;
42
+ }
43
+
44
+ /**
45
+ * @stable
46
+ */
47
+ export function createSkillsRoutes(deps: SkillsRoutesDeps): Hono<{ Variables: ServerVariables }> {
48
+ const app = new Hono<{ Variables: ServerVariables }>();
49
+
50
+ app.get('/', createScopeMiddleware('skills:read'), async (c) => {
51
+ return c.json({ skills: await deps.skills.list() });
52
+ });
53
+
54
+ app.get('/:name', createScopeMiddleware('skills:read'), async (c) => {
55
+ const name = c.req.param('name');
56
+ const skill = await deps.skills.get(name);
57
+ if (skill === null)
58
+ return c.json({ error: 'skill-not-found', message: `Skill '${name}' not found.` }, 404);
59
+ return c.json({ skill });
60
+ });
61
+
62
+ app.post('/install', createScopeMiddleware('skills:install'), async (c) => {
63
+ const parsed = InstallBodySchema.safeParse(await safelyParseJson(c));
64
+ if (!parsed.success) {
65
+ return c.json(
66
+ {
67
+ error: 'config-invalid',
68
+ message: 'Invalid install body.',
69
+ issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
70
+ },
71
+ 400,
72
+ );
73
+ }
74
+ const installInput: { source: string; trust?: 'verified' | 'unverified' } = {
75
+ source: parsed.data.source,
76
+ };
77
+ if (parsed.data.trust !== undefined) installInput.trust = parsed.data.trust;
78
+ const out = await deps.skills.install(installInput);
79
+ return c.json({ skill: out }, 201);
80
+ });
81
+
82
+ return app;
83
+ }
84
+
85
+ async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
86
+ try {
87
+ return await c.req.json();
88
+ } catch {
89
+ return {};
90
+ }
91
+ }