@opengeni/api-router 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +9 -1
- package/dist/app.js +7 -1
- package/dist/{chunk-3HIA43CC.js → chunk-HBEJMWD3.js} +5470 -2223
- package/dist/chunk-HBEJMWD3.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +279 -55
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/app.ts +583 -166
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +84 -8
- package/src/index.ts +178 -75
- package/src/integrations/oauth-client.ts +403 -120
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1600 -693
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +71 -33
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +870 -328
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +146 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/routes/documents.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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, {
|
|
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, {
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 =
|
|
238
|
-
|
|
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(
|
|
241
|
-
|
|
242
|
-
|
|
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 =
|
|
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(
|
|
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, {
|
|
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(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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) ──────
|