@opengeni/api-router 0.17.0 → 0.21.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/app.js +1 -1
- package/dist/{chunk-MWBF2GXL.js → chunk-5EEC7Q6C.js} +852 -111
- package/dist/chunk-5EEC7Q6C.js.map +1 -0
- package/dist/codex-realtime.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-drive.d.ts +16 -1
- package/dist/mcp/documents.d.ts +3 -3
- package/dist/workspace-state-projection.d.ts +29 -1
- package/package.json +11 -11
- package/src/app.ts +33 -2
- package/src/codex-realtime.ts +4 -0
- package/src/index.ts +3 -2
- package/src/integrations/google-drive.ts +385 -20
- package/src/mcp/documents.ts +49 -27
- package/src/routes/connections.ts +61 -9
- package/src/routes/documents.ts +134 -43
- package/src/routes/workspace-instruction-policies.ts +118 -0
- package/src/routes/workspace-state.ts +46 -1
- package/src/workspace-state-projection.ts +203 -4
- package/dist/chunk-MWBF2GXL.js.map +0 -1
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
15
15
|
GoogleDriveConnectionMetadata,
|
|
16
|
+
GoogleDriveDisconnectRequest,
|
|
17
|
+
GoogleDriveLifecycleActionRequest,
|
|
16
18
|
GoogleDriveOAuthStartRequest,
|
|
17
19
|
GoogleDriveOAuthStartResponse,
|
|
18
20
|
} from "@opengeni/contracts/google-drive";
|
|
@@ -47,8 +49,10 @@ import { HTTPException } from "hono/http-exception";
|
|
|
47
49
|
import {
|
|
48
50
|
browseGoogleDrive,
|
|
49
51
|
completeGoogleDriveOAuthCallback,
|
|
52
|
+
disconnectGoogleDrive,
|
|
50
53
|
saveGoogleDriveSource,
|
|
51
54
|
startGoogleDriveOAuth,
|
|
55
|
+
transitionGoogleDriveLifecycle,
|
|
52
56
|
} from "../integrations/google-drive";
|
|
53
57
|
import {
|
|
54
58
|
completeMcpOAuthCallback,
|
|
@@ -288,6 +292,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
288
292
|
return c.redirect(result.redirectTo, 302);
|
|
289
293
|
});
|
|
290
294
|
|
|
295
|
+
app.patch(
|
|
296
|
+
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
|
|
297
|
+
async (c) => {
|
|
298
|
+
assertIntegrationsEnabled();
|
|
299
|
+
const workspaceId = c.req.param("workspaceId");
|
|
300
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
301
|
+
const parsed = GoogleDriveLifecycleActionRequest.safeParse(await c.req.json());
|
|
302
|
+
if (!parsed.success) {
|
|
303
|
+
throw new HTTPException(400, { message: "invalid Google Drive lifecycle request" });
|
|
304
|
+
}
|
|
305
|
+
return c.json(
|
|
306
|
+
ConnectionResponse.parse({
|
|
307
|
+
connection: await transitionGoogleDriveLifecycle(deps, {
|
|
308
|
+
workspaceId,
|
|
309
|
+
subjectId: grant.subjectId,
|
|
310
|
+
connectionId: c.req.param("connectionId"),
|
|
311
|
+
payload: parsed.data,
|
|
312
|
+
}),
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
291
318
|
app.get(
|
|
292
319
|
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/browse",
|
|
293
320
|
async (c) => {
|
|
@@ -421,18 +448,43 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
421
448
|
if (!existing) {
|
|
422
449
|
throw new HTTPException(404, { message: "connection not found" });
|
|
423
450
|
}
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
451
|
+
const isGoogleDrive =
|
|
452
|
+
existing.subjectId === grant.subjectId &&
|
|
453
|
+
existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
454
|
+
existing.kind === "oauth2" &&
|
|
455
|
+
GoogleDriveConnectionMetadata.safeParse(existing.metadata).success;
|
|
456
|
+
const googleDriveDisconnect = isGoogleDrive
|
|
457
|
+
? GoogleDriveDisconnectRequest.safeParse(await c.req.json().catch(() => null))
|
|
458
|
+
: null;
|
|
459
|
+
if (googleDriveDisconnect && !googleDriveDisconnect.success) {
|
|
460
|
+
throw new HTTPException(400, {
|
|
461
|
+
message:
|
|
462
|
+
googleDriveDisconnect.error.issues[0]?.message ??
|
|
463
|
+
"invalid Google Drive disconnect request",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (existing.status === "revoked" && !isGoogleDrive) {
|
|
467
|
+
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
468
|
+
}
|
|
469
|
+
const connection = isGoogleDrive
|
|
470
|
+
? await disconnectGoogleDrive(deps, {
|
|
427
471
|
workspaceId,
|
|
428
472
|
subjectId: grant.subjectId,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
432
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
433
|
-
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
473
|
+
connection: existing,
|
|
474
|
+
payload: googleDriveDisconnect!.data,
|
|
434
475
|
})
|
|
435
|
-
:
|
|
476
|
+
: isOpenGeniSlackBotConnection(existing)
|
|
477
|
+
? await revokeConnectionWithSlackBotSuccessAudit(db, {
|
|
478
|
+
accountId: grant.accountId,
|
|
479
|
+
workspaceId,
|
|
480
|
+
subjectId: grant.subjectId,
|
|
481
|
+
connectionId,
|
|
482
|
+
expectedVersion: existing.version,
|
|
483
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
484
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
485
|
+
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
486
|
+
})
|
|
487
|
+
: await revokeConnection(db, workspaceId, connectionId, grant.subjectId, existing.version);
|
|
436
488
|
if (!connection) {
|
|
437
489
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
438
490
|
}
|
package/src/routes/documents.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Document,
|
|
7
7
|
DocumentBase,
|
|
8
8
|
DocumentSearchRequest,
|
|
9
|
+
DocumentSearchResponse,
|
|
9
10
|
KnowledgeMemory,
|
|
10
11
|
KnowledgeMemorySearchRequest,
|
|
11
12
|
MoveDocumentRequest,
|
|
@@ -34,12 +35,12 @@ import {
|
|
|
34
35
|
listDocuments,
|
|
35
36
|
moveDocumentToBase,
|
|
36
37
|
queueDocumentForReindex,
|
|
37
|
-
|
|
38
|
+
searchEffectiveDocuments,
|
|
38
39
|
} from "@opengeni/documents";
|
|
39
40
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
40
41
|
import type { Context, Hono } from "hono";
|
|
41
42
|
import { HTTPException } from "hono/http-exception";
|
|
42
|
-
import { requireAccessGrant } from "@opengeni/core";
|
|
43
|
+
import { requireAccessGrant, requireAccessGrantAuthorization } from "@opengeni/core";
|
|
43
44
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
44
45
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
45
46
|
import { buildDocumentsMcpServer } from "../mcp/documents";
|
|
@@ -85,7 +86,8 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
85
86
|
|
|
86
87
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
87
88
|
const workspaceId = c.req.param("workspaceId");
|
|
88
|
-
const
|
|
89
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
|
|
90
|
+
const { grant } = access;
|
|
89
91
|
if (!objectStorage) {
|
|
90
92
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
91
93
|
}
|
|
@@ -96,6 +98,11 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
96
98
|
quantity: 0,
|
|
97
99
|
});
|
|
98
100
|
const payload = AddDocumentRequest.parse(await c.req.json());
|
|
101
|
+
const organizationAuthorityGranted =
|
|
102
|
+
access.accountGrant?.permissions.includes("account:admin") === true;
|
|
103
|
+
if (payload.authorityKind === "organization" && !organizationAuthorityGranted) {
|
|
104
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
105
|
+
}
|
|
99
106
|
try {
|
|
100
107
|
const document = await addDocumentToBase(db, {
|
|
101
108
|
...payload,
|
|
@@ -103,6 +110,8 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
103
110
|
workspaceId,
|
|
104
111
|
baseId: c.req.param("baseId"),
|
|
105
112
|
createdBy: grant.subjectId,
|
|
113
|
+
initiatingSubjectId: grant.subjectId,
|
|
114
|
+
organizationAuthorityGranted,
|
|
106
115
|
access: { viewerSubjectId: grant.subjectId },
|
|
107
116
|
});
|
|
108
117
|
const wasCreated =
|
|
@@ -114,6 +123,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
114
123
|
accountId: grant.accountId,
|
|
115
124
|
workspaceId,
|
|
116
125
|
documentId: document.id,
|
|
126
|
+
authorityKind: document.authorityKind,
|
|
127
|
+
authorityWorkspaceId: document.authorityWorkspaceId,
|
|
128
|
+
authoritySubjectId: document.authoritySubjectId,
|
|
117
129
|
})) ?? document);
|
|
118
130
|
if (indexed.status === "ready") {
|
|
119
131
|
await recordWorkspaceUsage(deps, {
|
|
@@ -150,13 +162,28 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
150
162
|
"/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId",
|
|
151
163
|
async (c) => {
|
|
152
164
|
const workspaceId = c.req.param("workspaceId");
|
|
153
|
-
const
|
|
165
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
166
|
+
c,
|
|
167
|
+
deps,
|
|
168
|
+
workspaceId,
|
|
169
|
+
"documents:manage",
|
|
170
|
+
);
|
|
171
|
+
const { grant } = authorization;
|
|
172
|
+
const organizationAuthorityGranted = hasAccountAdminAuthority(authorization);
|
|
154
173
|
try {
|
|
174
|
+
const document = await getDocument(db, workspaceId, c.req.param("documentId"), {
|
|
175
|
+
viewerSubjectId: grant.subjectId,
|
|
176
|
+
});
|
|
177
|
+
if (!document || document.baseId !== c.req.param("baseId")) {
|
|
178
|
+
throw new HTTPException(404, { message: "document not found" });
|
|
179
|
+
}
|
|
180
|
+
requireOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
155
181
|
await deleteDocumentFromBase(db, {
|
|
156
182
|
accountId: grant.accountId,
|
|
157
183
|
workspaceId,
|
|
158
184
|
baseId: c.req.param("baseId"),
|
|
159
185
|
documentId: c.req.param("documentId"),
|
|
186
|
+
organizationAuthorityGranted,
|
|
160
187
|
access: { viewerSubjectId: grant.subjectId },
|
|
161
188
|
});
|
|
162
189
|
return c.body(null, 204);
|
|
@@ -173,7 +200,14 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
173
200
|
"/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId/reindex",
|
|
174
201
|
async (c) => {
|
|
175
202
|
const workspaceId = c.req.param("workspaceId");
|
|
176
|
-
const
|
|
203
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
204
|
+
c,
|
|
205
|
+
deps,
|
|
206
|
+
workspaceId,
|
|
207
|
+
"documents:manage",
|
|
208
|
+
);
|
|
209
|
+
const { grant } = authorization;
|
|
210
|
+
const organizationAuthorityGranted = hasAccountAdminAuthority(authorization);
|
|
177
211
|
if (!objectStorage) {
|
|
178
212
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
179
213
|
}
|
|
@@ -190,20 +224,30 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
190
224
|
if (!document) {
|
|
191
225
|
throw new HTTPException(404, { message: "document not found" });
|
|
192
226
|
}
|
|
227
|
+
requireOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
193
228
|
if (document.status !== "failed") {
|
|
194
229
|
throw new HTTPException(422, { message: "only failed documents can be retried" });
|
|
195
230
|
}
|
|
196
231
|
if (document.baseId !== c.req.param("baseId")) {
|
|
197
232
|
throw new HTTPException(404, { message: "document not found" });
|
|
198
233
|
}
|
|
199
|
-
const queued = await queueDocumentForReindex(
|
|
200
|
-
|
|
201
|
-
|
|
234
|
+
const queued = await queueDocumentForReindex(
|
|
235
|
+
db,
|
|
236
|
+
workspaceId,
|
|
237
|
+
document.id,
|
|
238
|
+
{
|
|
239
|
+
viewerSubjectId: grant.subjectId,
|
|
240
|
+
},
|
|
241
|
+
organizationAuthorityGranted,
|
|
242
|
+
);
|
|
202
243
|
const indexed =
|
|
203
244
|
(await documentIndexer.indexDocument({
|
|
204
245
|
accountId: grant.accountId,
|
|
205
246
|
workspaceId,
|
|
206
247
|
documentId: document.id,
|
|
248
|
+
authorityKind: document.authorityKind,
|
|
249
|
+
authorityWorkspaceId: document.authorityWorkspaceId,
|
|
250
|
+
authoritySubjectId: document.authoritySubjectId,
|
|
207
251
|
})) ?? queued;
|
|
208
252
|
if (indexed.status === "ready") {
|
|
209
253
|
await recordWorkspaceUsage(deps, {
|
|
@@ -236,44 +280,52 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
236
280
|
if (!base) {
|
|
237
281
|
throw new HTTPException(404, { message: "document base not found" });
|
|
238
282
|
}
|
|
239
|
-
return c.json(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
return c.json(
|
|
284
|
+
DocumentSearchResponse.parse({
|
|
285
|
+
results: await searchEffectiveDocuments(
|
|
286
|
+
db,
|
|
287
|
+
{
|
|
288
|
+
accountId: grant.accountId,
|
|
289
|
+
workspaceId,
|
|
290
|
+
baseIds: [base.id],
|
|
291
|
+
query: payload.query,
|
|
292
|
+
limit: payload.limit,
|
|
293
|
+
mode: payload.mode,
|
|
294
|
+
sourceKinds: payload.sourceKinds,
|
|
295
|
+
aclTags: payload.aclTags,
|
|
296
|
+
initiatingSubjectId: grant.subjectId,
|
|
297
|
+
surface: "human",
|
|
298
|
+
},
|
|
299
|
+
getDocumentServices(),
|
|
300
|
+
),
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
255
303
|
});
|
|
256
304
|
|
|
257
305
|
app.post("/v1/workspaces/:workspaceId/knowledge/search", async (c) => {
|
|
258
306
|
const workspaceId = c.req.param("workspaceId");
|
|
259
307
|
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
260
308
|
const payload = await parseDocumentSearchRequest(c, "invalid knowledge search request");
|
|
261
|
-
return c.json(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
return c.json(
|
|
310
|
+
DocumentSearchResponse.parse({
|
|
311
|
+
results: await searchEffectiveDocuments(
|
|
312
|
+
db,
|
|
313
|
+
{
|
|
314
|
+
accountId: grant.accountId,
|
|
315
|
+
workspaceId,
|
|
316
|
+
query: payload.query,
|
|
317
|
+
baseIds: payload.baseIds,
|
|
318
|
+
limit: payload.limit,
|
|
319
|
+
mode: payload.mode,
|
|
320
|
+
sourceKinds: payload.sourceKinds,
|
|
321
|
+
aclTags: payload.aclTags,
|
|
322
|
+
initiatingSubjectId: grant.subjectId,
|
|
323
|
+
surface: "human",
|
|
324
|
+
},
|
|
325
|
+
getDocumentServices(),
|
|
326
|
+
),
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
277
329
|
});
|
|
278
330
|
|
|
279
331
|
// Knowledge drop: raw text or an uploaded file, no metadata required. Lands
|
|
@@ -282,7 +334,8 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
282
334
|
// curation is disabled.
|
|
283
335
|
app.post("/v1/workspaces/:workspaceId/knowledge/drops", async (c) => {
|
|
284
336
|
const workspaceId = c.req.param("workspaceId");
|
|
285
|
-
const
|
|
337
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
|
|
338
|
+
const { grant } = access;
|
|
286
339
|
if (!objectStorage) {
|
|
287
340
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
288
341
|
}
|
|
@@ -293,6 +346,11 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
293
346
|
quantity: 0,
|
|
294
347
|
});
|
|
295
348
|
const payload = CreateKnowledgeDropRequest.parse(await c.req.json());
|
|
349
|
+
const organizationAuthorityGranted =
|
|
350
|
+
access.accountGrant?.permissions.includes("account:admin") === true;
|
|
351
|
+
if (payload.authorityKind === "organization" && !organizationAuthorityGranted) {
|
|
352
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
353
|
+
}
|
|
296
354
|
try {
|
|
297
355
|
let fileId: string;
|
|
298
356
|
if (payload.text !== undefined) {
|
|
@@ -353,12 +411,15 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
353
411
|
const document = await addDocumentToBase(db, {
|
|
354
412
|
fileId,
|
|
355
413
|
...(payload.title ? { title: payload.title } : {}),
|
|
414
|
+
...(payload.authorityKind ? { authorityKind: payload.authorityKind } : {}),
|
|
356
415
|
...(payload.visibility ? { visibility: payload.visibility } : {}),
|
|
357
416
|
...(payload.agentAccess !== undefined ? { agentAccess: payload.agentAccess } : {}),
|
|
358
417
|
accountId: grant.accountId,
|
|
359
418
|
workspaceId,
|
|
360
419
|
baseId: defaultBase.id,
|
|
361
420
|
createdBy: grant.subjectId,
|
|
421
|
+
initiatingSubjectId: grant.subjectId,
|
|
422
|
+
organizationAuthorityGranted,
|
|
362
423
|
curationStatus: "pending",
|
|
363
424
|
access: { viewerSubjectId: grant.subjectId },
|
|
364
425
|
});
|
|
@@ -371,6 +432,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
371
432
|
accountId: grant.accountId,
|
|
372
433
|
workspaceId,
|
|
373
434
|
documentId: document.id,
|
|
435
|
+
authorityKind: document.authorityKind,
|
|
436
|
+
authorityWorkspaceId: document.authorityWorkspaceId,
|
|
437
|
+
authoritySubjectId: document.authoritySubjectId,
|
|
374
438
|
})) ?? document);
|
|
375
439
|
if (indexed.status === "ready") {
|
|
376
440
|
await recordWorkspaceUsage(deps, {
|
|
@@ -397,7 +461,14 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
397
461
|
// Apply a curation suggestion (no body target) or move to an explicit base.
|
|
398
462
|
app.post("/v1/workspaces/:workspaceId/documents/:documentId/move", async (c) => {
|
|
399
463
|
const workspaceId = c.req.param("workspaceId");
|
|
400
|
-
const
|
|
464
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
465
|
+
c,
|
|
466
|
+
deps,
|
|
467
|
+
workspaceId,
|
|
468
|
+
"documents:manage",
|
|
469
|
+
);
|
|
470
|
+
const { grant } = authorization;
|
|
471
|
+
const organizationAuthorityGranted = hasAccountAdminAuthority(authorization);
|
|
401
472
|
const payload = MoveDocumentRequest.parse(await c.req.json().catch(() => ({})));
|
|
402
473
|
try {
|
|
403
474
|
const document = await getDocument(db, workspaceId, c.req.param("documentId"), {
|
|
@@ -406,6 +477,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
406
477
|
if (!document) {
|
|
407
478
|
throw new HTTPException(404, { message: "document not found" });
|
|
408
479
|
}
|
|
480
|
+
requireOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
409
481
|
return c.json(
|
|
410
482
|
Document.parse(
|
|
411
483
|
await moveDocumentToBase(db, {
|
|
@@ -413,6 +485,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
413
485
|
workspaceId,
|
|
414
486
|
documentId: document.id,
|
|
415
487
|
targetBaseId: payload.targetBaseId ?? null,
|
|
488
|
+
organizationAuthorityGranted,
|
|
416
489
|
access: { viewerSubjectId: grant.subjectId },
|
|
417
490
|
}),
|
|
418
491
|
),
|
|
@@ -565,7 +638,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
565
638
|
grant.accountId,
|
|
566
639
|
workspaceId,
|
|
567
640
|
getDocumentServices(),
|
|
568
|
-
{ createdBySessionId: sessionId,
|
|
641
|
+
{ createdBySessionId: sessionId, initiatingSubjectId: grant.subjectId },
|
|
569
642
|
);
|
|
570
643
|
await server.connect(transport);
|
|
571
644
|
return await transport.handleRequest(c.req.raw);
|
|
@@ -591,6 +664,9 @@ function dropFilename(preferred: string | undefined): string {
|
|
|
591
664
|
|
|
592
665
|
function documentHttpException(error: unknown): HTTPException {
|
|
593
666
|
const message = error instanceof Error ? error.message : String(error);
|
|
667
|
+
if (message.includes("organization document") && message.includes("exact account authority")) {
|
|
668
|
+
return new HTTPException(403, { message: "missing permission: account:admin" });
|
|
669
|
+
}
|
|
594
670
|
if (message.includes("not found")) {
|
|
595
671
|
return new HTTPException(404, { message });
|
|
596
672
|
}
|
|
@@ -615,3 +691,18 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
615
691
|
}
|
|
616
692
|
return new HTTPException(500, { message });
|
|
617
693
|
}
|
|
694
|
+
|
|
695
|
+
function hasAccountAdminAuthority(
|
|
696
|
+
authorization: Awaited<ReturnType<typeof requireAccessGrantAuthorization>>,
|
|
697
|
+
): boolean {
|
|
698
|
+
return authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function requireOrganizationDocumentAuthority(
|
|
702
|
+
authorityKind: string,
|
|
703
|
+
organizationAuthorityGranted: boolean,
|
|
704
|
+
): void {
|
|
705
|
+
if (authorityKind === "organization" && !organizationAuthorityGranted) {
|
|
706
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
ActivateWorkspaceInstructionPolicyRequest,
|
|
3
4
|
CreateWorkspaceInstructionPolicyDraftRequest,
|
|
5
|
+
CreateWorkspaceInstructionPolicyOnboardingProposalRequest,
|
|
4
6
|
ImportLegacyWorkspaceInstructionPolicyDraftRequest,
|
|
5
7
|
RollbackWorkspaceInstructionPolicyRequest,
|
|
6
8
|
WorkspaceInstructionPolicyActivationResponse,
|
|
@@ -9,21 +11,35 @@ import {
|
|
|
9
11
|
WorkspaceInstructionPolicyDiffResponse,
|
|
10
12
|
WorkspaceInstructionPolicyListQuery,
|
|
11
13
|
WorkspaceInstructionPolicyListResponse,
|
|
14
|
+
WorkspaceInstructionPolicyOnboardingProposal,
|
|
15
|
+
WorkspaceInstructionPolicyOnboardingProposalConflictResponse,
|
|
16
|
+
WorkspaceInstructionPolicyOnboardingProposalContentErrorResponse,
|
|
17
|
+
WorkspaceInstructionPolicyOnboardingProposalListQuery,
|
|
18
|
+
WorkspaceInstructionPolicyOnboardingProposalListResponse,
|
|
19
|
+
WorkspaceInstructionPolicyOnboardingProposalStaleResponse,
|
|
20
|
+
WorkspaceInstructionPolicyOperationReuseResponse,
|
|
12
21
|
WorkspaceInstructionPolicyRevision,
|
|
22
|
+
WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
|
|
13
23
|
} from "@opengeni/contracts";
|
|
14
24
|
import { requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
|
|
15
25
|
import {
|
|
16
26
|
activateWorkspaceInstructionPolicyRevision,
|
|
17
27
|
createWorkspaceInstructionPolicyDraft,
|
|
28
|
+
createWorkspaceInstructionPolicyOnboardingProposal,
|
|
18
29
|
diffWorkspaceInstructionPolicyRevisions,
|
|
19
30
|
getWorkspaceInstructionPolicyRevision,
|
|
20
31
|
importLegacyWorkspaceInstructionPolicyDraft,
|
|
21
32
|
listWorkspaceInstructionPolicyRevisions,
|
|
33
|
+
listWorkspaceInstructionPolicyOnboardingProposals,
|
|
22
34
|
rollbackWorkspaceInstructionPolicyRevision,
|
|
23
35
|
WorkspaceInstructionPolicyConflictError,
|
|
24
36
|
WorkspaceInstructionPolicyInvalidOperationError,
|
|
25
37
|
WorkspaceInstructionPolicyLegacyUnavailableError,
|
|
26
38
|
WorkspaceInstructionPolicyNotFoundError,
|
|
39
|
+
WorkspaceInstructionPolicyOnboardingProposalConflictError,
|
|
40
|
+
WorkspaceInstructionPolicyOnboardingProposalContentError,
|
|
41
|
+
WorkspaceInstructionPolicyOnboardingProposalStaleError,
|
|
42
|
+
WorkspaceInstructionPolicyOperationReuseError,
|
|
27
43
|
} from "@opengeni/db";
|
|
28
44
|
import type { Context, Hono } from "hono";
|
|
29
45
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -50,6 +66,46 @@ function policyErrorResponse(context: Context, error: unknown): Response {
|
|
|
50
66
|
409,
|
|
51
67
|
);
|
|
52
68
|
}
|
|
69
|
+
if (error instanceof WorkspaceInstructionPolicyOperationReuseError) {
|
|
70
|
+
return context.json(
|
|
71
|
+
WorkspaceInstructionPolicyOperationReuseResponse.parse({
|
|
72
|
+
code: error.code,
|
|
73
|
+
message: error.message,
|
|
74
|
+
}),
|
|
75
|
+
409,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (error instanceof WorkspaceInstructionPolicyOnboardingProposalContentError) {
|
|
79
|
+
return context.json(
|
|
80
|
+
WorkspaceInstructionPolicyOnboardingProposalContentErrorResponse.parse({
|
|
81
|
+
code: error.code,
|
|
82
|
+
message: error.message,
|
|
83
|
+
maxChars: WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
|
|
84
|
+
}),
|
|
85
|
+
422,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (error instanceof WorkspaceInstructionPolicyOnboardingProposalStaleError) {
|
|
89
|
+
return context.json(
|
|
90
|
+
WorkspaceInstructionPolicyOnboardingProposalStaleResponse.parse({
|
|
91
|
+
code: error.code,
|
|
92
|
+
message: error.message,
|
|
93
|
+
currentHead: error.currentHead,
|
|
94
|
+
}),
|
|
95
|
+
409,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (error instanceof WorkspaceInstructionPolicyOnboardingProposalConflictError) {
|
|
99
|
+
return context.json(
|
|
100
|
+
WorkspaceInstructionPolicyOnboardingProposalConflictResponse.parse({
|
|
101
|
+
code: error.code,
|
|
102
|
+
message: error.message,
|
|
103
|
+
existingProposalId: error.existingProposalId,
|
|
104
|
+
existingDraftRevisionId: error.existingDraftRevisionId,
|
|
105
|
+
}),
|
|
106
|
+
409,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
53
109
|
if (error instanceof WorkspaceInstructionPolicyNotFoundError) {
|
|
54
110
|
return context.json(
|
|
55
111
|
{ code: "WORKSPACE_INSTRUCTION_POLICY_NOT_FOUND", message: error.message },
|
|
@@ -117,6 +173,7 @@ export function registerWorkspaceInstructionPolicyRoutes(app: Hono, deps: ApiRou
|
|
|
117
173
|
return context.json(
|
|
118
174
|
WorkspaceInstructionPolicyRevision.parse(
|
|
119
175
|
await createWorkspaceInstructionPolicyDraft(deps.db, {
|
|
176
|
+
operationId: request.operationId ?? randomUUID(),
|
|
120
177
|
accountId: grant.accountId,
|
|
121
178
|
workspaceId,
|
|
122
179
|
createdBySubjectId: grant.subjectId,
|
|
@@ -145,6 +202,7 @@ export function registerWorkspaceInstructionPolicyRoutes(app: Hono, deps: ApiRou
|
|
|
145
202
|
return context.json(
|
|
146
203
|
WorkspaceInstructionPolicyRevision.parse(
|
|
147
204
|
await importLegacyWorkspaceInstructionPolicyDraft(deps.db, {
|
|
205
|
+
operationId: request.operationId ?? randomUUID(),
|
|
148
206
|
accountId: grant.accountId,
|
|
149
207
|
workspaceId,
|
|
150
208
|
createdBySubjectId: grant.subjectId,
|
|
@@ -158,6 +216,58 @@ export function registerWorkspaceInstructionPolicyRoutes(app: Hono, deps: ApiRou
|
|
|
158
216
|
}
|
|
159
217
|
});
|
|
160
218
|
|
|
219
|
+
app.get(`${base}/onboarding-proposals`, async (context) => {
|
|
220
|
+
const workspaceId = context.req.param("workspaceId");
|
|
221
|
+
await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
222
|
+
const parsed = WorkspaceInstructionPolicyOnboardingProposalListQuery.safeParse({
|
|
223
|
+
limit: context.req.query("limit"),
|
|
224
|
+
});
|
|
225
|
+
if (!parsed.success) {
|
|
226
|
+
throw new HTTPException(422, {
|
|
227
|
+
message: "Invalid workspace instruction-policy onboarding-proposal query",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return context.json(
|
|
231
|
+
WorkspaceInstructionPolicyOnboardingProposalListResponse.parse(
|
|
232
|
+
await listWorkspaceInstructionPolicyOnboardingProposals(deps.db, workspaceId, parsed.data),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app.post(`${base}/onboarding-proposals`, async (context) => {
|
|
238
|
+
const workspaceId = context.req.param("workspaceId");
|
|
239
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:admin");
|
|
240
|
+
assertBoundedActor(grant.subjectId);
|
|
241
|
+
const request = await parseBody(
|
|
242
|
+
context,
|
|
243
|
+
CreateWorkspaceInstructionPolicyOnboardingProposalRequest,
|
|
244
|
+
);
|
|
245
|
+
try {
|
|
246
|
+
return context.json(
|
|
247
|
+
WorkspaceInstructionPolicyOnboardingProposal.parse(
|
|
248
|
+
await createWorkspaceInstructionPolicyOnboardingProposal(deps.db, {
|
|
249
|
+
operationId: request.operationId ?? randomUUID(),
|
|
250
|
+
accountId: grant.accountId,
|
|
251
|
+
workspaceId,
|
|
252
|
+
createdBySubjectId: grant.subjectId,
|
|
253
|
+
kind: request.kind,
|
|
254
|
+
scope: request.scope,
|
|
255
|
+
roleKey: request.roleKey,
|
|
256
|
+
content: request.content,
|
|
257
|
+
sourceId: request.sourceId,
|
|
258
|
+
sourceVersion: request.sourceVersion,
|
|
259
|
+
confidenceBps: request.confidenceBps,
|
|
260
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
261
|
+
expectedActivationVersion: request.expectedActivationVersion,
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
201,
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return policyErrorResponse(context, error);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
161
271
|
app.get(`${base}/diff`, async (context) => {
|
|
162
272
|
const workspaceId = context.req.param("workspaceId");
|
|
163
273
|
await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
@@ -188,10 +298,14 @@ export function registerWorkspaceInstructionPolicyRoutes(app: Hono, deps: ApiRou
|
|
|
188
298
|
return context.json(
|
|
189
299
|
WorkspaceInstructionPolicyActivationResponse.parse(
|
|
190
300
|
await rollbackWorkspaceInstructionPolicyRevision(deps.db, {
|
|
301
|
+
operationId: request.operationId ?? randomUUID(),
|
|
191
302
|
accountId: grant.accountId,
|
|
192
303
|
workspaceId,
|
|
193
304
|
targetRevisionId: request.targetRevisionId,
|
|
194
305
|
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
306
|
+
...(request.expectedActivationVersion === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: { expectedActivationVersion: request.expectedActivationVersion }),
|
|
195
309
|
actorSubjectId: grant.subjectId,
|
|
196
310
|
reason: request.reason,
|
|
197
311
|
}),
|
|
@@ -227,10 +341,14 @@ export function registerWorkspaceInstructionPolicyRoutes(app: Hono, deps: ApiRou
|
|
|
227
341
|
return context.json(
|
|
228
342
|
WorkspaceInstructionPolicyActivationResponse.parse(
|
|
229
343
|
await activateWorkspaceInstructionPolicyRevision(deps.db, {
|
|
344
|
+
operationId: request.operationId ?? randomUUID(),
|
|
230
345
|
accountId: grant.accountId,
|
|
231
346
|
workspaceId,
|
|
232
347
|
revisionId,
|
|
233
348
|
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
349
|
+
...(request.expectedActivationVersion === undefined
|
|
350
|
+
? {}
|
|
351
|
+
: { expectedActivationVersion: request.expectedActivationVersion }),
|
|
234
352
|
actorSubjectId: grant.subjectId,
|
|
235
353
|
reason: request.reason,
|
|
236
354
|
}),
|