@opengeni/api-router 0.5.2 → 0.5.4

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 (48) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-YY6OAEL6.js → chunk-DO2G3JSB.js} +5333 -2205
  3. package/dist/chunk-DO2G3JSB.js.map +1 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +297 -54
  6. package/dist/index.js.map +1 -1
  7. package/package.json +21 -21
  8. package/src/app.ts +415 -147
  9. package/src/auth/managed-auth.ts +32 -16
  10. package/src/http/auth.ts +8 -1
  11. package/src/http/common.ts +6 -2
  12. package/src/http/sse.ts +27 -6
  13. package/src/index.ts +196 -74
  14. package/src/integrations/oauth-client.ts +592 -131
  15. package/src/integrations/provider-domain.ts +4 -1
  16. package/src/mcp/documents.ts +173 -94
  17. package/src/mcp/server.ts +1517 -692
  18. package/src/mcp/session-view.ts +8 -2
  19. package/src/mcp/toolspace.ts +175 -84
  20. package/src/observability.ts +7 -1
  21. package/src/routes/api-keys.ts +39 -23
  22. package/src/routes/billing.ts +180 -65
  23. package/src/routes/capabilities.ts +17 -8
  24. package/src/routes/catalog-assets.ts +5 -2
  25. package/src/routes/codex.ts +244 -63
  26. package/src/routes/connections.ts +72 -34
  27. package/src/routes/documents.ts +242 -92
  28. package/src/routes/enrollments.ts +100 -70
  29. package/src/routes/environments.ts +205 -136
  30. package/src/routes/files.ts +164 -39
  31. package/src/routes/github.ts +123 -50
  32. package/src/routes/install.ts +9 -2
  33. package/src/routes/machines.ts +9 -8
  34. package/src/routes/packs.ts +141 -89
  35. package/src/routes/rigs.ts +189 -0
  36. package/src/routes/scheduled-tasks.ts +51 -9
  37. package/src/routes/sessions.ts +839 -329
  38. package/src/routes/social.ts +50 -38
  39. package/src/routes/workspace-capture.ts +238 -0
  40. package/src/routes/workspaces.ts +159 -13
  41. package/src/sandbox/access.ts +11 -3
  42. package/src/sandbox/auth-callout.ts +5 -1
  43. package/src/sandbox/channel-a.ts +104 -27
  44. package/src/sandbox/enrollment.ts +13 -3
  45. package/src/sandbox/machines.ts +68 -59
  46. package/src/sandbox/metrics-ingestion.ts +238 -17
  47. package/src/sandbox/viewer.ts +172 -46
  48. package/dist/chunk-YY6OAEL6.js.map +0 -1
@@ -8,12 +8,16 @@ import {
8
8
  KnowledgeMemory,
9
9
  KnowledgeMemorySearchRequest,
10
10
  UpdateKnowledgeMemoryRequest,
11
+ WorkspaceMemorySearchRequest,
12
+ WorkspaceMemorySearchResponse,
11
13
  } from "@opengeni/contracts";
12
14
  import {
13
15
  createKnowledgeMemory,
14
16
  getKnowledgeMemory,
15
17
  listKnowledgeMemories,
16
18
  updateKnowledgeMemory,
19
+ saveWorkspaceMemory,
20
+ searchWorkspaceMemories,
17
21
  } from "@opengeni/db";
18
22
  import {
19
23
  addDocumentToBase,
@@ -41,13 +45,20 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
41
45
  const workspaceId = c.req.param("workspaceId");
42
46
  const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
43
47
  const payload = CreateDocumentBaseRequest.parse(await c.req.json());
44
- return c.json(DocumentBase.parse(await createDocumentBase(db, { ...payload, accountId: grant.accountId, workspaceId })), 201);
48
+ return c.json(
49
+ DocumentBase.parse(
50
+ await createDocumentBase(db, { ...payload, accountId: grant.accountId, workspaceId }),
51
+ ),
52
+ 201,
53
+ );
45
54
  });
46
55
 
47
56
  app.get("/v1/workspaces/:workspaceId/document-bases", async (c) => {
48
57
  const workspaceId = c.req.param("workspaceId");
49
58
  await requireAccessGrant(c, deps, workspaceId, "documents:search");
50
- return c.json((await listDocumentBases(db, workspaceId)).map((base) => DocumentBase.parse(base)));
59
+ return c.json(
60
+ (await listDocumentBases(db, workspaceId)).map((base) => DocumentBase.parse(base)),
61
+ );
51
62
  });
52
63
 
53
64
  app.get("/v1/workspaces/:workspaceId/document-bases/:baseId", async (c) => {
@@ -66,12 +77,30 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
66
77
  if (!objectStorage) {
67
78
  throw new HTTPException(503, { message: "object storage is not configured" });
68
79
  }
69
- await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
80
+ await requireLimit(deps, {
81
+ accountId: grant.accountId,
82
+ workspaceId,
83
+ action: "document:index",
84
+ quantity: 0,
85
+ });
70
86
  const payload = AddDocumentRequest.parse(await c.req.json());
71
87
  try {
72
- const document = await addDocumentToBase(db, { ...payload, accountId: grant.accountId, workspaceId, baseId: c.req.param("baseId") });
73
- const wasCreated = document.status === "queued" && document.chunkCount === 0 && document.error === null;
74
- const indexed = document.status === "ready" ? document : (await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? document);
88
+ const document = await addDocumentToBase(db, {
89
+ ...payload,
90
+ accountId: grant.accountId,
91
+ workspaceId,
92
+ baseId: c.req.param("baseId"),
93
+ });
94
+ const wasCreated =
95
+ document.status === "queued" && document.chunkCount === 0 && document.error === null;
96
+ const indexed =
97
+ document.status === "ready"
98
+ ? document
99
+ : ((await documentIndexer.indexDocument({
100
+ accountId: grant.accountId,
101
+ workspaceId,
102
+ documentId: document.id,
103
+ })) ?? document);
75
104
  if (indexed.status === "ready") {
76
105
  await recordWorkspaceUsage(deps, {
77
106
  accountId: grant.accountId,
@@ -94,66 +123,86 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
94
123
  app.get("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
95
124
  const workspaceId = c.req.param("workspaceId");
96
125
  await requireAccessGrant(c, deps, workspaceId, "documents:search");
97
- return c.json((await listDocuments(db, workspaceId, c.req.param("baseId"))).map((document) => Document.parse(document)));
98
- });
99
-
100
- app.delete("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId", async (c) => {
101
- const workspaceId = c.req.param("workspaceId");
102
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
103
- try {
104
- await deleteDocumentFromBase(db, {
105
- accountId: grant.accountId,
106
- workspaceId,
107
- baseId: c.req.param("baseId"),
108
- documentId: c.req.param("documentId"),
109
- });
110
- return c.body(null, 204);
111
- } catch (error) {
112
- throw documentHttpException(error);
113
- }
126
+ return c.json(
127
+ (await listDocuments(db, workspaceId, c.req.param("baseId"))).map((document) =>
128
+ Document.parse(document),
129
+ ),
130
+ );
114
131
  });
115
132
 
116
- app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId/reindex", async (c) => {
117
- const workspaceId = c.req.param("workspaceId");
118
- const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
119
- if (!objectStorage) {
120
- throw new HTTPException(503, { message: "object storage is not configured" });
121
- }
122
- await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
123
- try {
124
- const document = await getDocument(db, workspaceId, c.req.param("documentId"));
125
- if (!document) {
126
- throw new HTTPException(404, { message: "document not found" });
127
- }
128
- if (document.status !== "failed") {
129
- throw new HTTPException(422, { message: "only failed documents can be retried" });
130
- }
131
- if (document.baseId !== c.req.param("baseId")) {
132
- throw new HTTPException(404, { message: "document not found" });
133
- }
134
- const queued = await queueDocumentForReindex(db, workspaceId, document.id);
135
- const indexed = await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? queued;
136
- if (indexed.status === "ready") {
137
- await recordWorkspaceUsage(deps, {
133
+ app.delete(
134
+ "/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId",
135
+ async (c) => {
136
+ const workspaceId = c.req.param("workspaceId");
137
+ const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
138
+ try {
139
+ await deleteDocumentFromBase(db, {
138
140
  accountId: grant.accountId,
139
141
  workspaceId,
140
- subjectId: grant.subjectId,
141
- eventType: "document.indexed",
142
- quantity: indexed.chunkCount,
143
- unit: "chunk",
144
- sourceResourceType: "document",
145
- sourceResourceId: indexed.id,
146
- idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
142
+ baseId: c.req.param("baseId"),
143
+ documentId: c.req.param("documentId"),
147
144
  });
145
+ return c.body(null, 204);
146
+ } catch (error) {
147
+ throw documentHttpException(error);
148
148
  }
149
- return c.json(Document.parse(indexed));
150
- } catch (error) {
151
- if (error instanceof HTTPException) {
152
- throw error;
149
+ },
150
+ );
151
+
152
+ app.post(
153
+ "/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId/reindex",
154
+ async (c) => {
155
+ const workspaceId = c.req.param("workspaceId");
156
+ const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
157
+ if (!objectStorage) {
158
+ throw new HTTPException(503, { message: "object storage is not configured" });
153
159
  }
154
- throw documentHttpException(error);
155
- }
156
- });
160
+ await requireLimit(deps, {
161
+ accountId: grant.accountId,
162
+ workspaceId,
163
+ action: "document:index",
164
+ quantity: 0,
165
+ });
166
+ try {
167
+ const document = await getDocument(db, workspaceId, c.req.param("documentId"));
168
+ if (!document) {
169
+ throw new HTTPException(404, { message: "document not found" });
170
+ }
171
+ if (document.status !== "failed") {
172
+ throw new HTTPException(422, { message: "only failed documents can be retried" });
173
+ }
174
+ if (document.baseId !== c.req.param("baseId")) {
175
+ throw new HTTPException(404, { message: "document not found" });
176
+ }
177
+ const queued = await queueDocumentForReindex(db, workspaceId, document.id);
178
+ const indexed =
179
+ (await documentIndexer.indexDocument({
180
+ accountId: grant.accountId,
181
+ workspaceId,
182
+ documentId: document.id,
183
+ })) ?? queued;
184
+ if (indexed.status === "ready") {
185
+ await recordWorkspaceUsage(deps, {
186
+ accountId: grant.accountId,
187
+ workspaceId,
188
+ subjectId: grant.subjectId,
189
+ eventType: "document.indexed",
190
+ quantity: indexed.chunkCount,
191
+ unit: "chunk",
192
+ sourceResourceType: "document",
193
+ sourceResourceId: indexed.id,
194
+ idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
195
+ });
196
+ }
197
+ return c.json(Document.parse(indexed));
198
+ } catch (error) {
199
+ if (error instanceof HTTPException) {
200
+ throw error;
201
+ }
202
+ throw documentHttpException(error);
203
+ }
204
+ },
205
+ );
157
206
 
158
207
  app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/search", async (c) => {
159
208
  const workspaceId = c.req.param("workspaceId");
@@ -164,15 +213,19 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
164
213
  throw new HTTPException(404, { message: "document base not found" });
165
214
  }
166
215
  return c.json({
167
- results: await searchDocuments(db, {
168
- workspaceId,
169
- baseIds: [base.id],
170
- query: payload.query,
171
- limit: payload.limit,
172
- mode: payload.mode,
173
- sourceKinds: payload.sourceKinds,
174
- aclTags: payload.aclTags,
175
- }, getDocumentServices()),
216
+ results: await searchDocuments(
217
+ db,
218
+ {
219
+ workspaceId,
220
+ baseIds: [base.id],
221
+ query: payload.query,
222
+ limit: payload.limit,
223
+ mode: payload.mode,
224
+ sourceKinds: payload.sourceKinds,
225
+ aclTags: payload.aclTags,
226
+ },
227
+ getDocumentServices(),
228
+ ),
176
229
  });
177
230
  });
178
231
 
@@ -181,15 +234,19 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
181
234
  await requireAccessGrant(c, deps, workspaceId, "documents:search");
182
235
  const payload = DocumentSearchRequest.parse(await c.req.json());
183
236
  return c.json({
184
- results: await searchDocuments(db, {
185
- workspaceId,
186
- query: payload.query,
187
- baseIds: payload.baseIds,
188
- limit: payload.limit,
189
- mode: payload.mode,
190
- sourceKinds: payload.sourceKinds,
191
- aclTags: payload.aclTags,
192
- }, getDocumentServices()),
237
+ results: await searchDocuments(
238
+ db,
239
+ {
240
+ workspaceId,
241
+ query: payload.query,
242
+ baseIds: payload.baseIds,
243
+ limit: payload.limit,
244
+ mode: payload.mode,
245
+ sourceKinds: payload.sourceKinds,
246
+ aclTags: payload.aclTags,
247
+ },
248
+ getDocumentServices(),
249
+ ),
193
250
  });
194
251
  });
195
252
 
@@ -206,7 +263,11 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
206
263
  if (!parsed.success) {
207
264
  throw new HTTPException(400, { message: "invalid knowledge memory query parameters" });
208
265
  }
209
- return c.json((await listKnowledgeMemories(db, workspaceId, parsed.data)).map((memory) => KnowledgeMemory.parse(memory)));
266
+ return c.json(
267
+ (await listKnowledgeMemories(db, workspaceId, parsed.data)).map((memory) =>
268
+ KnowledgeMemory.parse(memory),
269
+ ),
270
+ );
210
271
  });
211
272
 
212
273
  app.get("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
@@ -219,28 +280,100 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
219
280
  return c.json(KnowledgeMemory.parse(memory));
220
281
  });
221
282
 
283
+ // Hybrid search over the workspace's agent-visible memory (active ∪ approved).
284
+ // Available regardless of the workspace memory setting (human/audit lane).
285
+ app.post("/v1/workspaces/:workspaceId/knowledge/memories/search", async (c) => {
286
+ const workspaceId = c.req.param("workspaceId");
287
+ await requireAccessGrant(c, deps, workspaceId, "documents:search");
288
+ const parsed = WorkspaceMemorySearchRequest.safeParse(await c.req.json());
289
+ if (!parsed.success) {
290
+ throw new HTTPException(400, { message: "invalid workspace memory search request" });
291
+ }
292
+ const results = await searchWorkspaceMemories(
293
+ db,
294
+ workspaceId,
295
+ parsed.data,
296
+ getDocumentServices().embedder,
297
+ );
298
+ return c.json(
299
+ WorkspaceMemorySearchResponse.parse({
300
+ results: results.map((result) => ({
301
+ ...result,
302
+ memory: KnowledgeMemory.parse(result.memory),
303
+ })),
304
+ }),
305
+ );
306
+ });
307
+
222
308
  app.post("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
223
309
  const workspaceId = c.req.param("workspaceId");
224
310
  const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
225
- const payload = CreateKnowledgeMemoryRequest.parse(await c.req.json());
226
- return c.json(KnowledgeMemory.parse(await createKnowledgeMemory(db, {
227
- ...payload,
228
- accountId: grant.accountId,
229
- workspaceId,
230
- })), 201);
311
+ const parsedBody = CreateKnowledgeMemoryRequest.safeParse(await c.req.json());
312
+ if (!parsedBody.success) {
313
+ throw new HTTPException(400, { message: "invalid knowledge memory request" });
314
+ }
315
+ const payload = parsedBody.data;
316
+ // status `active` (the default) is a memory write → route through the single
317
+ // gate (sanitize + embed + dedup). Explicit proposed/approved/rejected keeps
318
+ // the legacy curated create.
319
+ if (payload.status === "active") {
320
+ try {
321
+ const result = await saveWorkspaceMemory(
322
+ db,
323
+ {
324
+ accountId: grant.accountId,
325
+ workspaceId,
326
+ text: payload.text,
327
+ kind: payload.kind,
328
+ confidence: payload.confidence,
329
+ pinned: payload.pinned,
330
+ replacesId: payload.replacesId ?? null,
331
+ metadata: payload.metadata,
332
+ origin: "human",
333
+ },
334
+ getDocumentServices().embedder,
335
+ );
336
+ return c.json(KnowledgeMemory.parse(result.memory), 201);
337
+ } catch (error) {
338
+ throw documentHttpException(error);
339
+ }
340
+ }
341
+ return c.json(
342
+ KnowledgeMemory.parse(
343
+ await createKnowledgeMemory(db, {
344
+ ...payload,
345
+ accountId: grant.accountId,
346
+ workspaceId,
347
+ }),
348
+ ),
349
+ 201,
350
+ );
231
351
  });
232
352
 
233
353
  app.patch("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
234
354
  const workspaceId = c.req.param("workspaceId");
235
355
  const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
236
356
  const payload = UpdateKnowledgeMemoryRequest.parse(await c.req.json());
237
- const reviewedBy = payload.reviewedBy
238
- ?? (payload.status === "approved" || payload.status === "rejected" ? grant.subjectLabel ?? grant.subjectId : undefined);
357
+ const reviewedBy =
358
+ payload.reviewedBy ??
359
+ (payload.status === "approved" || payload.status === "rejected"
360
+ ? (grant.subjectLabel ?? grant.subjectId)
361
+ : undefined);
239
362
  try {
240
- return c.json(KnowledgeMemory.parse(await updateKnowledgeMemory(db, workspaceId, c.req.param("memoryId"), {
241
- ...payload,
242
- ...(reviewedBy ? { reviewedBy } : {}),
243
- })));
363
+ return c.json(
364
+ KnowledgeMemory.parse(
365
+ await updateKnowledgeMemory(
366
+ db,
367
+ workspaceId,
368
+ c.req.param("memoryId"),
369
+ {
370
+ ...payload,
371
+ ...(reviewedBy ? { reviewedBy } : {}),
372
+ },
373
+ getDocumentServices().embedder,
374
+ ),
375
+ ),
376
+ );
244
377
  } catch (error) {
245
378
  throw documentHttpException(error);
246
379
  }
@@ -249,9 +382,16 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
249
382
  app.all("/v1/workspaces/:workspaceId/mcp/docs", async (c) => {
250
383
  const workspaceId = c.req.param("workspaceId");
251
384
  const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
252
- const sessionId = typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : undefined;
385
+ const sessionId =
386
+ typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : undefined;
253
387
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
254
- const server = buildDocumentsMcpServer(db, grant.accountId, workspaceId, getDocumentServices(), { createdBySessionId: sessionId });
388
+ const server = buildDocumentsMcpServer(
389
+ db,
390
+ grant.accountId,
391
+ workspaceId,
392
+ getDocumentServices(),
393
+ { createdBySessionId: sessionId },
394
+ );
255
395
  await server.connect(transport);
256
396
  return await transport.handleRequest(c.req.raw);
257
397
  });
@@ -265,5 +405,15 @@ function documentHttpException(error: unknown): HTTPException {
265
405
  if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
266
406
  return new HTTPException(422, { message });
267
407
  }
408
+ // Workspace-memory write-gate rejections are client errors, not server faults.
409
+ if (
410
+ message.includes("too long") ||
411
+ message.includes("visible memory is full") ||
412
+ message.includes("empty after sanitization") ||
413
+ message.includes("does not match") ||
414
+ message.includes("Ambiguous memory id")
415
+ ) {
416
+ return new HTTPException(400, { message });
417
+ }
268
418
  return new HTTPException(500, { message });
269
419
  }
@@ -39,11 +39,7 @@ import {
39
39
  type EnrollmentArch,
40
40
  type EnrollmentOs,
41
41
  } from "@opengeni/contracts";
42
- import {
43
- getWorkspace,
44
- listEnrollments,
45
- revokeEnrollment,
46
- } from "@opengeni/db";
42
+ import { getWorkspace, listEnrollments, revokeEnrollment } from "@opengeni/db";
47
43
  import type { Context, Hono } from "hono";
48
44
  import { HTTPException } from "hono/http-exception";
49
45
  import { requireAccessGrant } from "@opengeni/core";
@@ -66,7 +62,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
66
62
  // surface invisible while disabled — it does not exist for this deployment yet.
67
63
  function assertSelfhostedEnabled(): void {
68
64
  if (!settings.sandboxSelfhostedEnabled) {
69
- throw new HTTPException(404, { message: "selfhosted enrollment is not enabled for this deployment" });
65
+ throw new HTTPException(404, {
66
+ message: "selfhosted enrollment is not enabled for this deployment",
67
+ });
70
68
  }
71
69
  }
72
70
 
@@ -106,18 +104,21 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
106
104
  if (!workspace) {
107
105
  throw new HTTPException(404, { message: "workspace not found" });
108
106
  }
109
- const result = await startDeviceEnrollment({ db, settings }, {
110
- accountId: workspace.accountId,
111
- workspaceId: workspace.id,
112
- publicKey: body.publicKey,
113
- os: body.os as EnrollmentOs,
114
- arch: body.arch as EnrollmentArch,
115
- machineName: body.machineName ?? null,
116
- canOfferDisplay: body.canOfferDisplay,
117
- requestsScreenControl: body.requestsScreenControl,
118
- // The approve page is served at the SAME origin as this request.
119
- verificationOrigin: new URL(c.req.url).origin,
120
- });
107
+ const result = await startDeviceEnrollment(
108
+ { db, settings },
109
+ {
110
+ accountId: workspace.accountId,
111
+ workspaceId: workspace.id,
112
+ publicKey: body.publicKey,
113
+ os: body.os as EnrollmentOs,
114
+ arch: body.arch as EnrollmentArch,
115
+ machineName: body.machineName ?? null,
116
+ canOfferDisplay: body.canOfferDisplay,
117
+ requestsScreenControl: body.requestsScreenControl,
118
+ // The approve page is served at the SAME origin as this request.
119
+ verificationOrigin: new URL(c.req.url).origin,
120
+ },
121
+ );
121
122
  return c.json(result, 201);
122
123
  });
123
124
 
@@ -129,7 +130,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
129
130
  if (!parsed.success) {
130
131
  throw new HTTPException(400, { message: "invalid device-poll request" });
131
132
  }
132
- const result = await pollDeviceEnrollment({ db, settings }, { deviceCode: parsed.data.deviceCode });
133
+ const result = await pollDeviceEnrollment(
134
+ { db, settings },
135
+ { deviceCode: parsed.data.deviceCode },
136
+ );
133
137
  return c.json(result, 200);
134
138
  });
135
139
 
@@ -146,7 +150,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
146
150
  if (!parsed.success) {
147
151
  throw new HTTPException(400, { message: "invalid device-lookup request" });
148
152
  }
149
- const record = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
153
+ const record = await lookupDeviceEnrollment(
154
+ { db, settings },
155
+ { userCode: parsed.data.userCode },
156
+ );
150
157
  if (!record) {
151
158
  // Unknown / terminal / expired code → 404 (indistinguishable from an
152
159
  // unauthorized one below, by design).
@@ -177,14 +184,17 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
177
184
  throw new HTTPException(400, { message: "invalid enroll-token-exchange request" });
178
185
  }
179
186
  const body = parsed.data;
180
- const result = await exchangeEnrollToken({ db, settings }, {
181
- token: body.token,
182
- publicKey: body.publicKey,
183
- os: body.os as EnrollmentOs,
184
- arch: body.arch as EnrollmentArch,
185
- machineName: body.machineName ?? null,
186
- canOfferDisplay: body.canOfferDisplay,
187
- });
187
+ const result = await exchangeEnrollToken(
188
+ { db, settings },
189
+ {
190
+ token: body.token,
191
+ publicKey: body.publicKey,
192
+ os: body.os as EnrollmentOs,
193
+ arch: body.arch as EnrollmentArch,
194
+ machineName: body.machineName ?? null,
195
+ canOfferDisplay: body.canOfferDisplay,
196
+ },
197
+ );
188
198
  if (!result.ok) {
189
199
  if (result.reason === "disabled") {
190
200
  // The credential plane is off for this deployment (no signing secret).
@@ -207,25 +217,31 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
207
217
  throw new HTTPException(400, { message: "invalid device-approve request" });
208
218
  }
209
219
  const body = parsed.data;
210
- const approved = await approveDeviceEnrollment({ db, settings }, {
211
- accountId: grant.accountId,
212
- workspaceId,
213
- userCode: body.userCode,
214
- allowScreenControl: body.allowScreenControl,
215
- // The LOUD consent record: WHO consented (the authenticated subject + label).
216
- approvedBySubjectId: grant.subjectId,
217
- approvedBySubjectLabel: grant.subjectLabel ?? null,
218
- });
220
+ const approved = await approveDeviceEnrollment(
221
+ { db, settings },
222
+ {
223
+ accountId: grant.accountId,
224
+ workspaceId,
225
+ userCode: body.userCode,
226
+ allowScreenControl: body.allowScreenControl,
227
+ // The LOUD consent record: WHO consented (the authenticated subject + label).
228
+ approvedBySubjectId: grant.subjectId,
229
+ approvedBySubjectLabel: grant.subjectLabel ?? null,
230
+ },
231
+ );
219
232
  if (!approved) {
220
233
  // An unknown / expired / already-terminal user_code in this workspace.
221
234
  throw new HTTPException(404, { message: "no pending enrollment for that code" });
222
235
  }
223
- return c.json(DeviceEnrollmentApproveResponse.parse({
224
- approved: true,
225
- enrollmentId: approved.enrollmentId,
226
- sandboxId: approved.sandboxId,
227
- allowScreenControl: approved.allowScreenControl,
228
- }), 201);
236
+ return c.json(
237
+ DeviceEnrollmentApproveResponse.parse({
238
+ approved: true,
239
+ enrollmentId: approved.enrollmentId,
240
+ sandboxId: approved.sandboxId,
241
+ allowScreenControl: approved.allowScreenControl,
242
+ }),
243
+ 201,
244
+ );
229
245
  });
230
246
 
231
247
  // ── POST /workspaces/:workspaceId/enrollments/device/deny (user-authed) ─────
@@ -239,11 +255,14 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
239
255
  if (!parsed.success) {
240
256
  throw new HTTPException(400, { message: "invalid device-deny request" });
241
257
  }
242
- const result = await denyDeviceEnrollment({ db, settings }, {
243
- accountId: grant.accountId,
244
- workspaceId,
245
- userCode: parsed.data.userCode,
246
- });
258
+ const result = await denyDeviceEnrollment(
259
+ { db, settings },
260
+ {
261
+ accountId: grant.accountId,
262
+ workspaceId,
263
+ userCode: parsed.data.userCode,
264
+ },
265
+ );
247
266
  return c.json(DeviceEnrollmentDenyResponse.parse({ denied: result.denied }), 200);
248
267
  });
249
268
 
@@ -261,11 +280,14 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
261
280
  if (!parsed.success) {
262
281
  throw new HTTPException(400, { message: "invalid mint-enroll-token request" });
263
282
  }
264
- const minted = await mintEnrollToken({ db, settings }, {
265
- accountId: grant.accountId,
266
- workspaceId,
267
- allowScreenControl: parsed.data.allowScreenControl,
268
- });
283
+ const minted = await mintEnrollToken(
284
+ { db, settings },
285
+ {
286
+ accountId: grant.accountId,
287
+ workspaceId,
288
+ allowScreenControl: parsed.data.allowScreenControl,
289
+ },
290
+ );
269
291
  if (!minted) {
270
292
  // The credential plane is off (no signing secret) — mirror poll's disabled path.
271
293
  throw new HTTPException(503, { message: "enrollment credential plane is not configured" });
@@ -279,23 +301,31 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
279
301
  await requireAccessGrant(c, deps, workspaceId, "enrollments:read");
280
302
  assertSelfhostedEnabled();
281
303
  const statusFilter = c.req.query("status");
282
- const rows = await listEnrollments(db, workspaceId, statusFilter === "active" ? { status: "active" } : {});
283
- return c.json(ListEnrollmentsResponse.parse({
284
- enrollments: rows.map((row) => EnrollmentSummary.parse({
285
- id: row.id,
286
- pubkey: row.pubkey,
287
- exposure: row.exposure,
288
- hasDisplay: row.hasDisplay,
289
- desktopUnavailableReason: row.desktopUnavailableReason,
290
- allowScreenControl: row.allowScreenControl,
291
- status: row.status,
292
- os: row.os,
293
- arch: row.arch,
294
- lastSeenAt: row.lastSeenAt,
295
- createdAt: row.createdAt,
296
- revokedAt: row.revokedAt,
297
- })),
298
- }));
304
+ const rows = await listEnrollments(
305
+ db,
306
+ workspaceId,
307
+ statusFilter === "active" ? { status: "active" } : {},
308
+ );
309
+ return c.json(
310
+ ListEnrollmentsResponse.parse({
311
+ enrollments: rows.map((row) =>
312
+ EnrollmentSummary.parse({
313
+ id: row.id,
314
+ pubkey: row.pubkey,
315
+ exposure: row.exposure,
316
+ hasDisplay: row.hasDisplay,
317
+ desktopUnavailableReason: row.desktopUnavailableReason,
318
+ allowScreenControl: row.allowScreenControl,
319
+ status: row.status,
320
+ os: row.os,
321
+ arch: row.arch,
322
+ lastSeenAt: row.lastSeenAt,
323
+ createdAt: row.createdAt,
324
+ revokedAt: row.revokedAt,
325
+ }),
326
+ ),
327
+ }),
328
+ );
299
329
  });
300
330
 
301
331
  // ── POST /workspaces/:workspaceId/enrollments/:id/revoke (user-authed) ──────