@opengeni/api-router 0.30.3 → 2.1.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-QKDFBBUE.js} +26385 -15573
- package/dist/chunk-QKDFBBUE.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +149 -9
- package/dist/integrations/slack-interactions.d.ts +4 -3
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +153 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +337 -57
- package/src/integrations/slack-interactions.ts +1265 -208
- package/src/integrations/slack-routing.ts +324 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +221 -3
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
package/src/routes/documents.ts
CHANGED
|
@@ -4,17 +4,26 @@ import {
|
|
|
4
4
|
CreateKnowledgeMemoryRequest,
|
|
5
5
|
CreateDocumentBaseRequest,
|
|
6
6
|
Document,
|
|
7
|
+
DocumentAuthorityReclassification,
|
|
7
8
|
DocumentBase,
|
|
9
|
+
DocumentDefaultCollectionBackfill,
|
|
8
10
|
DocumentSearchRequest,
|
|
9
11
|
DocumentSearchResponse,
|
|
12
|
+
FileAsset,
|
|
13
|
+
FileDownloadUrlResponse,
|
|
10
14
|
KnowledgeMemory,
|
|
11
15
|
KnowledgeMemorySearchRequest,
|
|
16
|
+
ListDocumentAuthorityReclassificationsQuery,
|
|
17
|
+
ListDocumentAuthorityReclassificationsResponse,
|
|
12
18
|
MoveDocumentRequest,
|
|
19
|
+
ReclassifyDocumentAuthorityRequest,
|
|
20
|
+
RunDocumentDefaultCollectionBackfillRequest,
|
|
13
21
|
UpdateKnowledgeMemoryRequest,
|
|
14
22
|
WorkspaceMemorySearchRequest,
|
|
15
23
|
WorkspaceMemorySearchResponse,
|
|
16
24
|
} from "@opengeni/contracts";
|
|
17
25
|
import {
|
|
26
|
+
recordAuditEvent,
|
|
18
27
|
completeFileUpload,
|
|
19
28
|
createFileUpload,
|
|
20
29
|
createKnowledgeMemory,
|
|
@@ -29,11 +38,16 @@ import {
|
|
|
29
38
|
deleteDocumentFromBase,
|
|
30
39
|
ensureDefaultBase,
|
|
31
40
|
getDocument,
|
|
41
|
+
getDocumentOriginalFile,
|
|
32
42
|
getDocumentBase,
|
|
43
|
+
listAccessibleDocuments,
|
|
44
|
+
listDocumentAuthorityReclassifications,
|
|
33
45
|
listDocumentBasesEnsuringDefault,
|
|
34
46
|
listDocuments,
|
|
35
47
|
moveDocumentToBase,
|
|
36
48
|
queueDocumentForReindex,
|
|
49
|
+
reclassifyDocumentAuthority,
|
|
50
|
+
runDocumentDefaultCollectionBackfill,
|
|
37
51
|
searchEffectiveDocuments,
|
|
38
52
|
} from "@opengeni/documents";
|
|
39
53
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -41,6 +55,7 @@ import type { Context, Hono } from "hono";
|
|
|
41
55
|
import { HTTPException } from "hono/http-exception";
|
|
42
56
|
import {
|
|
43
57
|
requireAccessGrant,
|
|
58
|
+
requireAccountAdminAuthorizationStamp,
|
|
44
59
|
requireAccessGrantAuthorization,
|
|
45
60
|
saveWorkspaceMemoryWithSlackPublication,
|
|
46
61
|
} from "@opengeni/core";
|
|
@@ -87,6 +102,36 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
87
102
|
return c.json(DocumentBase.parse(base));
|
|
88
103
|
});
|
|
89
104
|
|
|
105
|
+
app.post("/v1/workspaces/:workspaceId/document-default-collection-backfills", async (c) => {
|
|
106
|
+
const workspaceId = c.req.param("workspaceId");
|
|
107
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
108
|
+
c,
|
|
109
|
+
deps,
|
|
110
|
+
workspaceId,
|
|
111
|
+
"documents:manage",
|
|
112
|
+
);
|
|
113
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
114
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
115
|
+
}
|
|
116
|
+
const accountAdminAuthorization = requireAccountAdminAuthorizationStamp(authorization);
|
|
117
|
+
const payload = RunDocumentDefaultCollectionBackfillRequest.parse(await c.req.json());
|
|
118
|
+
try {
|
|
119
|
+
return c.json(
|
|
120
|
+
DocumentDefaultCollectionBackfill.parse(
|
|
121
|
+
await runDocumentDefaultCollectionBackfill(db, {
|
|
122
|
+
...payload,
|
|
123
|
+
accountId: authorization.grant.accountId,
|
|
124
|
+
workspaceId,
|
|
125
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
126
|
+
accountAdminAuthorization,
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw documentHttpException(error);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
90
135
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
91
136
|
const workspaceId = c.req.param("workspaceId");
|
|
92
137
|
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
|
|
@@ -161,6 +206,148 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
161
206
|
);
|
|
162
207
|
});
|
|
163
208
|
|
|
209
|
+
app.get("/v1/workspaces/:workspaceId/documents", async (c) => {
|
|
210
|
+
const workspaceId = c.req.param("workspaceId");
|
|
211
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
212
|
+
return c.json(
|
|
213
|
+
(
|
|
214
|
+
await listAccessibleDocuments(db, workspaceId, {
|
|
215
|
+
viewerSubjectId: grant.subjectId,
|
|
216
|
+
})
|
|
217
|
+
).map((document) => Document.parse(document)),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
app.post(
|
|
222
|
+
"/v1/workspaces/:workspaceId/documents/:documentId/authority-reclassifications",
|
|
223
|
+
async (c) => {
|
|
224
|
+
const workspaceId = c.req.param("workspaceId");
|
|
225
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
226
|
+
c,
|
|
227
|
+
deps,
|
|
228
|
+
workspaceId,
|
|
229
|
+
"documents:manage",
|
|
230
|
+
);
|
|
231
|
+
const payload = ReclassifyDocumentAuthorityRequest.parse(await c.req.json());
|
|
232
|
+
const accountAdminAuthorized = hasAccountAdminAuthority(authorization);
|
|
233
|
+
const accountAdminAuthorization = accountAdminAuthorized
|
|
234
|
+
? requireAccountAdminAuthorizationStamp(authorization)
|
|
235
|
+
: null;
|
|
236
|
+
try {
|
|
237
|
+
const document = await getDocument(db, workspaceId, c.req.param("documentId"), {
|
|
238
|
+
viewerSubjectId: authorization.grant.subjectId,
|
|
239
|
+
});
|
|
240
|
+
if (!document) throw new HTTPException(404, { message: "document not found" });
|
|
241
|
+
if (
|
|
242
|
+
(document.authorityKind === "organization" ||
|
|
243
|
+
payload.targetAuthorityKind === "organization") &&
|
|
244
|
+
!accountAdminAuthorization
|
|
245
|
+
) {
|
|
246
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
247
|
+
}
|
|
248
|
+
return c.json(
|
|
249
|
+
DocumentAuthorityReclassification.parse(
|
|
250
|
+
await reclassifyDocumentAuthority(db, {
|
|
251
|
+
...payload,
|
|
252
|
+
accountId: authorization.grant.accountId,
|
|
253
|
+
workspaceId,
|
|
254
|
+
documentId: document.id,
|
|
255
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
256
|
+
accountAdminAuthorization,
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof HTTPException) throw error;
|
|
262
|
+
throw documentHttpException(error);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
app.get(
|
|
268
|
+
"/v1/workspaces/:workspaceId/documents/:documentId/authority-reclassifications",
|
|
269
|
+
async (c) => {
|
|
270
|
+
const workspaceId = c.req.param("workspaceId");
|
|
271
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
|
|
272
|
+
try {
|
|
273
|
+
const query = ListDocumentAuthorityReclassificationsQuery.parse(c.req.query());
|
|
274
|
+
return c.json(
|
|
275
|
+
ListDocumentAuthorityReclassificationsResponse.parse(
|
|
276
|
+
await listDocumentAuthorityReclassifications(db, {
|
|
277
|
+
accountId: grant.accountId,
|
|
278
|
+
workspaceId,
|
|
279
|
+
documentId: c.req.param("documentId"),
|
|
280
|
+
actorSubjectId: grant.subjectId,
|
|
281
|
+
...query,
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw documentHttpException(error);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
app.get("/v1/workspaces/:workspaceId/documents/:documentId/original-file", async (c) => {
|
|
292
|
+
const workspaceId = c.req.param("workspaceId");
|
|
293
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
294
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
295
|
+
const file = await getDocumentOriginalFile(db, {
|
|
296
|
+
accountId: grant.accountId,
|
|
297
|
+
workspaceId,
|
|
298
|
+
documentId: c.req.param("documentId"),
|
|
299
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
300
|
+
});
|
|
301
|
+
if (!file) {
|
|
302
|
+
throw new HTTPException(404, { message: "document file not found" });
|
|
303
|
+
}
|
|
304
|
+
return c.json(FileAsset.parse(file));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
app.post(
|
|
308
|
+
"/v1/workspaces/:workspaceId/documents/:documentId/original-file/download-url",
|
|
309
|
+
async (c) => {
|
|
310
|
+
const workspaceId = c.req.param("workspaceId");
|
|
311
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
312
|
+
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
313
|
+
if (!objectStorage) {
|
|
314
|
+
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
315
|
+
}
|
|
316
|
+
const file = await getDocumentOriginalFile(db, {
|
|
317
|
+
accountId: grant.accountId,
|
|
318
|
+
workspaceId,
|
|
319
|
+
documentId: c.req.param("documentId"),
|
|
320
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
321
|
+
});
|
|
322
|
+
if (!file) {
|
|
323
|
+
throw new HTTPException(404, { message: "document file not found" });
|
|
324
|
+
}
|
|
325
|
+
if (file.status !== "ready") {
|
|
326
|
+
throw new HTTPException(409, { message: `file is ${file.status}` });
|
|
327
|
+
}
|
|
328
|
+
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
329
|
+
await recordAuditEvent(db, {
|
|
330
|
+
accountId: grant.accountId,
|
|
331
|
+
workspaceId,
|
|
332
|
+
subjectId: grant.subjectId,
|
|
333
|
+
action: "file.signed_url.issued",
|
|
334
|
+
targetType: "workspace_document",
|
|
335
|
+
targetId: c.req.param("documentId"),
|
|
336
|
+
metadata: {
|
|
337
|
+
fileId: file.id,
|
|
338
|
+
kind: "document_original",
|
|
339
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
return c.json(
|
|
343
|
+
FileDownloadUrlResponse.parse({
|
|
344
|
+
url: signed.url,
|
|
345
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
164
351
|
app.delete(
|
|
165
352
|
"/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId",
|
|
166
353
|
async (c) => {
|
|
@@ -212,7 +399,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
212
399
|
const { grant } = authorization;
|
|
213
400
|
const organizationAuthorityGranted = hasAccountAdminAuthority(authorization);
|
|
214
401
|
if (!objectStorage) {
|
|
215
|
-
throw new HTTPException(503, {
|
|
402
|
+
throw new HTTPException(503, {
|
|
403
|
+
message: "object storage is not configured",
|
|
404
|
+
});
|
|
216
405
|
}
|
|
217
406
|
await requireLimit(deps, {
|
|
218
407
|
accountId: grant.accountId,
|
|
@@ -246,7 +435,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
246
435
|
const indexed =
|
|
247
436
|
(await documentIndexer.indexDocument({
|
|
248
437
|
accountId: grant.accountId,
|
|
249
|
-
|
|
438
|
+
// The requested workspace authorizes the human operation; the
|
|
439
|
+
// immutable ingestion workspace remains the indexing authority.
|
|
440
|
+
workspaceId: queued.workspaceId,
|
|
250
441
|
documentId: document.id,
|
|
251
442
|
authorityKind: document.authorityKind,
|
|
252
443
|
authorityWorkspaceId: document.authorityWorkspaceId,
|
|
@@ -651,13 +842,19 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
651
842
|
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
652
843
|
const sessionId =
|
|
653
844
|
typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : undefined;
|
|
845
|
+
const attemptId =
|
|
846
|
+
typeof grant.metadata?.attemptId === "string" ? grant.metadata.attemptId : undefined;
|
|
654
847
|
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
655
848
|
const server = buildDocumentsMcpServer(
|
|
656
849
|
db,
|
|
657
850
|
grant.accountId,
|
|
658
851
|
workspaceId,
|
|
659
852
|
getDocumentServices(),
|
|
660
|
-
{
|
|
853
|
+
{
|
|
854
|
+
createdBySessionId: sessionId,
|
|
855
|
+
attemptId,
|
|
856
|
+
initiatingSubjectId: grant.subjectId,
|
|
857
|
+
},
|
|
661
858
|
);
|
|
662
859
|
await server.connect(transport);
|
|
663
860
|
return await transport.handleRequest(c.req.raw);
|
|
@@ -692,6 +889,27 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
692
889
|
if (message.includes("already exists")) {
|
|
693
890
|
return new HTTPException(409, { message });
|
|
694
891
|
}
|
|
892
|
+
if (
|
|
893
|
+
message.includes("operation id was reused") ||
|
|
894
|
+
message.includes("authority changed before reclassification") ||
|
|
895
|
+
message.includes("run identity mismatch")
|
|
896
|
+
) {
|
|
897
|
+
return new HTTPException(409, { message });
|
|
898
|
+
}
|
|
899
|
+
if (
|
|
900
|
+
message.includes("reclassification requires") ||
|
|
901
|
+
message.includes("backfill requires organization administration")
|
|
902
|
+
) {
|
|
903
|
+
return new HTTPException(403, { message });
|
|
904
|
+
}
|
|
905
|
+
if (
|
|
906
|
+
message.includes("reclassification input is invalid") ||
|
|
907
|
+
message.includes("reclassification authority is invalid") ||
|
|
908
|
+
message.includes("invalid document authority receipt cursor") ||
|
|
909
|
+
message.includes("document authority receipt limit")
|
|
910
|
+
) {
|
|
911
|
+
return new HTTPException(400, { message });
|
|
912
|
+
}
|
|
695
913
|
if (message.includes("no suggested base")) {
|
|
696
914
|
return new HTTPException(422, { message });
|
|
697
915
|
}
|
|
@@ -78,9 +78,13 @@ const ListEditableArtifactsQuery = z
|
|
|
78
78
|
const MintEditableArtifactLiveTicketRequest = z
|
|
79
79
|
.object({
|
|
80
80
|
replicaId: ReplicaId,
|
|
81
|
-
|
|
81
|
+
modality: z.enum(["document", "spreadsheet", "presentation"]),
|
|
82
|
+
liveProtocolVersion: PositiveProtocolVersion,
|
|
82
83
|
kernelVersion: VersionName,
|
|
83
84
|
modelSchemaVersion: PositiveModelSchemaVersion,
|
|
85
|
+
snapshotVersion: PositiveProtocolVersion,
|
|
86
|
+
commandProtocolVersion: PositiveProtocolVersion,
|
|
87
|
+
committedTransactionProtocolVersion: PositiveProtocolVersion,
|
|
84
88
|
})
|
|
85
89
|
.strict();
|
|
86
90
|
|
|
@@ -482,9 +486,13 @@ export function registerEditableArtifactRoutes(
|
|
|
482
486
|
scope,
|
|
483
487
|
actor,
|
|
484
488
|
artifactId,
|
|
485
|
-
|
|
489
|
+
modality: body.modality,
|
|
490
|
+
liveProtocolVersion: body.liveProtocolVersion,
|
|
486
491
|
kernelVersion: body.kernelVersion,
|
|
487
492
|
modelSchemaVersion: body.modelSchemaVersion,
|
|
493
|
+
snapshotVersion: body.snapshotVersion,
|
|
494
|
+
commandProtocolVersion: body.commandProtocolVersion,
|
|
495
|
+
committedTransactionProtocolVersion: body.committedTransactionProtocolVersion,
|
|
488
496
|
allowEdit: hasPermission(grant.permissions, "artifacts:publish"),
|
|
489
497
|
});
|
|
490
498
|
} catch (error) {
|
|
@@ -496,7 +504,7 @@ export function registerEditableArtifactRoutes(
|
|
|
496
504
|
!parsedTicket.success ||
|
|
497
505
|
parsedTicket.data.artifactId !== artifactId ||
|
|
498
506
|
parsedTicket.data.replicaId !== actor.replicaId ||
|
|
499
|
-
parsedTicket.data.protocolVersion !== body.
|
|
507
|
+
parsedTicket.data.protocolVersion !== body.liveProtocolVersion ||
|
|
500
508
|
Date.parse(parsedTicket.data.expiresAt) <= Date.now()
|
|
501
509
|
) {
|
|
502
510
|
throw new Error("Editable artifact application returned an invalid live ticket");
|
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
} from "@opengeni/db";
|
|
51
51
|
import type { Context, Hono } from "hono";
|
|
52
52
|
import { HTTPException } from "hono/http-exception";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
requireAccessGrant,
|
|
55
|
+
requireAccessGrantAuthorization,
|
|
56
|
+
requirePermission,
|
|
57
|
+
} from "@opengeni/core";
|
|
54
58
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
55
59
|
import {
|
|
56
60
|
approveDeviceEnrollment,
|
|
@@ -88,7 +92,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
88
92
|
const lookupLimiter = new TokenBucket({ capacity: 30, refillPerSecond: 1 });
|
|
89
93
|
// The headless token exchange (UNAUTHENTICATED — the token is the auth). Bounded
|
|
90
94
|
// against an enroll-token brute force; the `oget_` HMAC is the real boundary.
|
|
91
|
-
const exchangeLimiter = new TokenBucket({
|
|
95
|
+
const exchangeLimiter = new TokenBucket({
|
|
96
|
+
capacity: 20,
|
|
97
|
+
refillPerSecond: 0.5,
|
|
98
|
+
});
|
|
92
99
|
|
|
93
100
|
function rateLimit(c: Context, limiter: TokenBucket): void {
|
|
94
101
|
const ip = clientIp(c);
|
|
@@ -123,8 +130,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
123
130
|
machineName: body.machineName ?? null,
|
|
124
131
|
canOfferDisplay: body.canOfferDisplay,
|
|
125
132
|
requestsScreenControl: body.requestsScreenControl,
|
|
126
|
-
//
|
|
127
|
-
|
|
133
|
+
// Prefer the externally configured deployment origin. Behind a TLS-
|
|
134
|
+
// terminating proxy c.req.url can carry the internal http scheme, which
|
|
135
|
+
// must never leak into the device-flow command shown to the operator.
|
|
136
|
+
verificationOrigin: settings.publicBaseUrl ?? new URL(c.req.url).origin,
|
|
128
137
|
},
|
|
129
138
|
);
|
|
130
139
|
return c.json(result, 201);
|
|
@@ -156,7 +165,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
156
165
|
rateLimit(c, lookupLimiter);
|
|
157
166
|
const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
|
|
158
167
|
if (!parsed.success) {
|
|
159
|
-
throw new HTTPException(400, {
|
|
168
|
+
throw new HTTPException(400, {
|
|
169
|
+
message: "invalid device-lookup request",
|
|
170
|
+
});
|
|
160
171
|
}
|
|
161
172
|
const record = await lookupDeviceEnrollment(
|
|
162
173
|
{ db, settings },
|
|
@@ -165,7 +176,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
165
176
|
if (!record) {
|
|
166
177
|
// Unknown / terminal / expired code → 404 (indistinguishable from an
|
|
167
178
|
// unauthorized one below, by design).
|
|
168
|
-
throw new HTTPException(404, {
|
|
179
|
+
throw new HTTPException(404, {
|
|
180
|
+
message: "no pending enrollment for that code",
|
|
181
|
+
});
|
|
169
182
|
}
|
|
170
183
|
// Authorize the caller against the RESOLVED workspace. A missing grant throws
|
|
171
184
|
// 403/404 from requireAccessGrant; we normalize that to 404 so a caller cannot
|
|
@@ -173,7 +186,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
173
186
|
try {
|
|
174
187
|
await requireAccessGrant(c, deps, record.workspaceId, "enrollments:read");
|
|
175
188
|
} catch {
|
|
176
|
-
throw new HTTPException(404, {
|
|
189
|
+
throw new HTTPException(404, {
|
|
190
|
+
message: "no pending enrollment for that code",
|
|
191
|
+
});
|
|
177
192
|
}
|
|
178
193
|
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record)), 200);
|
|
179
194
|
});
|
|
@@ -189,7 +204,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
189
204
|
rateLimit(c, exchangeLimiter);
|
|
190
205
|
const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
|
|
191
206
|
if (!parsed.success) {
|
|
192
|
-
throw new HTTPException(400, {
|
|
207
|
+
throw new HTTPException(400, {
|
|
208
|
+
message: "invalid enroll-token-exchange request",
|
|
209
|
+
});
|
|
193
210
|
}
|
|
194
211
|
const body = parsed.data;
|
|
195
212
|
const result = await exchangeEnrollToken(
|
|
@@ -206,10 +223,14 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
206
223
|
if (!result.ok) {
|
|
207
224
|
if (result.reason === "disabled") {
|
|
208
225
|
// The credential plane is off for this deployment (no signing secret).
|
|
209
|
-
throw new HTTPException(503, {
|
|
226
|
+
throw new HTTPException(503, {
|
|
227
|
+
message: "enrollment credential plane is not configured",
|
|
228
|
+
});
|
|
210
229
|
}
|
|
211
230
|
// An invalid / expired / wrong-typ token — the token is the auth, so 401.
|
|
212
|
-
throw new HTTPException(401, {
|
|
231
|
+
throw new HTTPException(401, {
|
|
232
|
+
message: "invalid or expired enroll token",
|
|
233
|
+
});
|
|
213
234
|
}
|
|
214
235
|
return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
|
|
215
236
|
});
|
|
@@ -218,18 +239,48 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
218
239
|
// The LOUD CONSENT step. requireAccessGrant BEFORE the Zod parse.
|
|
219
240
|
app.post("/v1/workspaces/:workspaceId/enrollments/device/approve", async (c) => {
|
|
220
241
|
const workspaceId = c.req.param("workspaceId");
|
|
221
|
-
const
|
|
242
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
243
|
+
const grant = authorization.grant;
|
|
244
|
+
requirePermission(grant, "enrollments:manage");
|
|
222
245
|
assertSelfhostedEnabled();
|
|
223
|
-
const
|
|
246
|
+
const rawBody = await c.req.json().catch(() => null);
|
|
247
|
+
const parsed = DeviceEnrollmentApproveRequest.safeParse(rawBody);
|
|
224
248
|
if (!parsed.success) {
|
|
225
|
-
throw new HTTPException(400, {
|
|
249
|
+
throw new HTTPException(400, {
|
|
250
|
+
message: "invalid device-approve request",
|
|
251
|
+
});
|
|
226
252
|
}
|
|
227
253
|
const body = parsed.data;
|
|
254
|
+
const scopeWasExplicit =
|
|
255
|
+
typeof rawBody === "object" &&
|
|
256
|
+
rawBody !== null &&
|
|
257
|
+
Object.prototype.hasOwnProperty.call(rawBody, "scope");
|
|
258
|
+
// Managed browser humans get the personal default. Legacy/delegated
|
|
259
|
+
// principals predate organization-user ownership and retain the compatible
|
|
260
|
+
// workspace default when the caller omitted scope; an explicit scope is
|
|
261
|
+
// never silently rewritten.
|
|
262
|
+
const effectiveScope =
|
|
263
|
+
scopeWasExplicit ||
|
|
264
|
+
(grant.principalKind === "human_session" &&
|
|
265
|
+
authorization.contextIntegrity &&
|
|
266
|
+
grant.metadata?.delegated !== true)
|
|
267
|
+
? body.scope
|
|
268
|
+
: undefined;
|
|
269
|
+
const allowOrganization =
|
|
270
|
+
effectiveScope === "organization" &&
|
|
271
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
272
|
+
if (effectiveScope === "organization" && !allowOrganization) {
|
|
273
|
+
throw new HTTPException(403, {
|
|
274
|
+
message: "missing permission: account:admin",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
228
277
|
const approved = await approveDeviceEnrollment(
|
|
229
278
|
{ db, settings },
|
|
230
279
|
{
|
|
231
280
|
accountId: grant.accountId,
|
|
232
281
|
workspaceId,
|
|
282
|
+
...(effectiveScope ? { scope: effectiveScope } : {}),
|
|
283
|
+
allowOrganization,
|
|
233
284
|
userCode: body.userCode,
|
|
234
285
|
allowScreenControl: body.allowScreenControl,
|
|
235
286
|
// The LOUD consent record: WHO consented (the authenticated subject + label).
|
|
@@ -239,7 +290,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
239
290
|
);
|
|
240
291
|
if (!approved) {
|
|
241
292
|
// An unknown / expired / already-terminal user_code in this workspace.
|
|
242
|
-
throw new HTTPException(404, {
|
|
293
|
+
throw new HTTPException(404, {
|
|
294
|
+
message: "no pending enrollment for that code",
|
|
295
|
+
});
|
|
243
296
|
}
|
|
244
297
|
return c.json(
|
|
245
298
|
DeviceEnrollmentApproveResponse.parse({
|
|
@@ -286,7 +339,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
286
339
|
// allowScreenControl=false). Coalesce a missing/empty body to {} before parse.
|
|
287
340
|
const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
288
341
|
if (!parsed.success) {
|
|
289
|
-
throw new HTTPException(400, {
|
|
342
|
+
throw new HTTPException(400, {
|
|
343
|
+
message: "invalid mint-enroll-token request",
|
|
344
|
+
});
|
|
290
345
|
}
|
|
291
346
|
const minted = await mintEnrollToken(
|
|
292
347
|
{ db, settings },
|
|
@@ -298,7 +353,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
298
353
|
);
|
|
299
354
|
if (!minted) {
|
|
300
355
|
// The credential plane is off (no signing secret) — mirror poll's disabled path.
|
|
301
|
-
throw new HTTPException(503, {
|
|
356
|
+
throw new HTTPException(503, {
|
|
357
|
+
message: "enrollment credential plane is not configured",
|
|
358
|
+
});
|
|
302
359
|
}
|
|
303
360
|
return c.json(MintEnrollTokenResponse.parse(minted), 201);
|
|
304
361
|
});
|
|
@@ -306,13 +363,15 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
306
363
|
// ── GET /workspaces/:workspaceId/enrollments (user-authed) ──────────────────
|
|
307
364
|
app.get("/v1/workspaces/:workspaceId/enrollments", async (c) => {
|
|
308
365
|
const workspaceId = c.req.param("workspaceId");
|
|
309
|
-
await requireAccessGrant(c, deps, workspaceId, "enrollments:read");
|
|
366
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "enrollments:read");
|
|
310
367
|
assertSelfhostedEnabled();
|
|
311
368
|
const statusFilter = c.req.query("status");
|
|
312
369
|
if (statusFilter !== undefined && statusFilter !== "active" && statusFilter !== "revoked") {
|
|
313
|
-
throw new HTTPException(400, {
|
|
370
|
+
throw new HTTPException(400, {
|
|
371
|
+
message: "status must be active or revoked",
|
|
372
|
+
});
|
|
314
373
|
}
|
|
315
|
-
const rows = await listEnrollments(db,
|
|
374
|
+
const rows = await listEnrollments(db, grant, {
|
|
316
375
|
status: statusFilter === "revoked" ? "revoked" : "active",
|
|
317
376
|
});
|
|
318
377
|
return c.json(
|
|
@@ -320,6 +379,8 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
320
379
|
enrollments: rows.map((row) =>
|
|
321
380
|
EnrollmentSummary.parse({
|
|
322
381
|
id: row.id,
|
|
382
|
+
scope: row.scope,
|
|
383
|
+
generation: row.generation,
|
|
323
384
|
pubkey: row.pubkey,
|
|
324
385
|
exposure: row.exposure,
|
|
325
386
|
hasDisplay: row.hasDisplay,
|
|
@@ -340,19 +401,40 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
340
401
|
// ── POST /workspaces/:workspaceId/enrollments/:id/revoke (user-authed) ──────
|
|
341
402
|
app.post("/v1/workspaces/:workspaceId/enrollments/:enrollmentId/revoke", async (c) => {
|
|
342
403
|
const workspaceId = c.req.param("workspaceId");
|
|
343
|
-
const
|
|
404
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
405
|
+
const grant = authorization.grant;
|
|
406
|
+
requirePermission(grant, "enrollments:manage");
|
|
344
407
|
assertSelfhostedEnabled();
|
|
345
408
|
const parsed = RemoveEnrollmentRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
346
409
|
if (!parsed.success) {
|
|
347
|
-
throw new HTTPException(400, {
|
|
410
|
+
throw new HTTPException(400, {
|
|
411
|
+
message: "invalid machine removal request",
|
|
412
|
+
});
|
|
348
413
|
}
|
|
349
414
|
const idempotencyKey =
|
|
350
415
|
parsed.data.idempotencyKey?.trim() || c.req.header("idempotency-key")?.trim() || randomUUID();
|
|
351
416
|
try {
|
|
417
|
+
const enrollmentId = c.req.param("enrollmentId");
|
|
418
|
+
const enrollment = (await listEnrollments(db, grant, { status: "active" })).find(
|
|
419
|
+
(candidate) => candidate.id === enrollmentId,
|
|
420
|
+
);
|
|
421
|
+
if (!enrollment) {
|
|
422
|
+
throw new HTTPException(404, {
|
|
423
|
+
message: "machine enrollment not found",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (
|
|
427
|
+
enrollment.scope === "organization" &&
|
|
428
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
429
|
+
) {
|
|
430
|
+
throw new HTTPException(403, {
|
|
431
|
+
message: "missing permission: account:admin",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
352
434
|
const result = await removeEnrollment(db, {
|
|
353
435
|
accountId: grant.accountId,
|
|
354
|
-
workspaceId,
|
|
355
|
-
enrollmentId
|
|
436
|
+
workspaceId: enrollment.workspaceId,
|
|
437
|
+
enrollmentId,
|
|
356
438
|
operationKey: idempotencyKey,
|
|
357
439
|
...(parsed.data.expectedUpdatedAt
|
|
358
440
|
? { expectedUpdatedAt: parsed.data.expectedUpdatedAt }
|
|
@@ -360,7 +442,9 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
360
442
|
subjectId: grant.subjectId,
|
|
361
443
|
});
|
|
362
444
|
if (!result) {
|
|
363
|
-
throw new HTTPException(404, {
|
|
445
|
+
throw new HTTPException(404, {
|
|
446
|
+
message: "machine enrollment not found",
|
|
447
|
+
});
|
|
364
448
|
}
|
|
365
449
|
return c.json(
|
|
366
450
|
RevokeEnrollmentResponse.parse({
|
|
@@ -376,7 +460,10 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
376
460
|
if (error instanceof MachineRemovalRevisionConflictError) {
|
|
377
461
|
throw new HTTPException(409, {
|
|
378
462
|
message: error.message,
|
|
379
|
-
cause: {
|
|
463
|
+
cause: {
|
|
464
|
+
code: "stale_revision",
|
|
465
|
+
currentUpdatedAt: error.currentUpdatedAt,
|
|
466
|
+
},
|
|
380
467
|
});
|
|
381
468
|
}
|
|
382
469
|
throw error;
|
|
@@ -410,7 +497,10 @@ class TokenBucket {
|
|
|
410
497
|
}
|
|
411
498
|
|
|
412
499
|
take(key: string, now = Date.now()): boolean {
|
|
413
|
-
const bucket = this.buckets.get(key) ?? {
|
|
500
|
+
const bucket = this.buckets.get(key) ?? {
|
|
501
|
+
tokens: this.capacity,
|
|
502
|
+
updatedAt: now,
|
|
503
|
+
};
|
|
414
504
|
const elapsedSeconds = Math.max(0, (now - bucket.updatedAt) / 1000);
|
|
415
505
|
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsedSeconds * this.refillPerSecond);
|
|
416
506
|
bucket.updatedAt = now;
|