@opengeni/api-router 0.22.2 → 0.26.1
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 +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
ATLASSIAN_PROVIDER_DOMAIN,
|
|
4
|
+
AtlassianConnectionMetadata,
|
|
5
|
+
AtlassianDisconnectRequest,
|
|
6
|
+
AtlassianLifecycleActionRequest,
|
|
7
|
+
AtlassianOAuthStartRequest,
|
|
8
|
+
AtlassianOAuthStartResponse,
|
|
9
|
+
} from "@opengeni/contracts/atlassian";
|
|
10
|
+
import {
|
|
11
|
+
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
12
|
+
ApiIntegrationOAuthStartRequest,
|
|
3
13
|
ConnectionResponse,
|
|
4
14
|
CreateConnectionRequest,
|
|
5
15
|
IntegrationClientMetadata,
|
|
16
|
+
ListSlackInstallationBindingsResponse,
|
|
6
17
|
ListConnectionsResponse,
|
|
7
18
|
OpenGeniSlackBotInstallRequest,
|
|
8
19
|
OpenGeniSlackBotInstallStart,
|
|
@@ -10,6 +21,10 @@ import {
|
|
|
10
21
|
OAuthStartResponse,
|
|
11
22
|
UpdateConnectionRequest,
|
|
12
23
|
} from "@opengeni/contracts";
|
|
24
|
+
import {
|
|
25
|
+
bindConnectorDocumentDestination,
|
|
26
|
+
ConnectorDocumentDestinationSelection,
|
|
27
|
+
} from "@opengeni/contracts/connector-destinations";
|
|
13
28
|
import {
|
|
14
29
|
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
15
30
|
GoogleDriveConnectionMetadata,
|
|
@@ -30,17 +45,19 @@ import {
|
|
|
30
45
|
import {
|
|
31
46
|
consumeIntegrationOAuthStateNonce,
|
|
32
47
|
createConnection,
|
|
33
|
-
createConnectionWithSlackBotSuccessAudit,
|
|
34
48
|
encryptEnvironmentValue,
|
|
35
49
|
getConnectionMetadata,
|
|
36
50
|
getWorkspaceGrant,
|
|
37
51
|
listConnectionsMetadata,
|
|
52
|
+
listSlackInstallationBindings,
|
|
53
|
+
persistSlackBotInstallationWithSuccessAudit,
|
|
38
54
|
recordSlackBotInstallCallbackFailure,
|
|
39
55
|
revokeConnection,
|
|
40
56
|
revokeConnectionWithSlackBotSuccessAudit,
|
|
41
57
|
SlackBotLifecycleSuccessAuditError,
|
|
58
|
+
SlackInstallationBindingConflictError,
|
|
42
59
|
updateConnection,
|
|
43
|
-
|
|
60
|
+
updateSlackBotDocumentDestination,
|
|
44
61
|
type SlackBotInstallCallbackFailureReason,
|
|
45
62
|
type SlackBotInstallCallbackFailureStage,
|
|
46
63
|
} from "@opengeni/db";
|
|
@@ -55,6 +72,19 @@ import {
|
|
|
55
72
|
startGoogleDriveOAuth,
|
|
56
73
|
transitionGoogleDriveLifecycle,
|
|
57
74
|
} from "../integrations/google-drive";
|
|
75
|
+
import {
|
|
76
|
+
completeApiIntegrationProviderOAuth,
|
|
77
|
+
isApiIntegrationProviderOAuthState,
|
|
78
|
+
startApiIntegrationProviderOAuth,
|
|
79
|
+
} from "../integrations/provider-oauth";
|
|
80
|
+
import {
|
|
81
|
+
browseAtlassianSources,
|
|
82
|
+
completeAtlassianOAuthCallback,
|
|
83
|
+
disconnectAtlassian,
|
|
84
|
+
saveAtlassianSources,
|
|
85
|
+
startAtlassianOAuth,
|
|
86
|
+
transitionAtlassianLifecycle,
|
|
87
|
+
} from "../integrations/atlassian";
|
|
58
88
|
import {
|
|
59
89
|
completeMcpOAuthCallback,
|
|
60
90
|
integrationBaseUrl,
|
|
@@ -104,16 +134,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
104
134
|
);
|
|
105
135
|
});
|
|
106
136
|
|
|
137
|
+
app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
|
|
138
|
+
const workspaceId = c.req.param("workspaceId");
|
|
139
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
140
|
+
return c.json(
|
|
141
|
+
ListSlackInstallationBindingsResponse.parse({
|
|
142
|
+
bindings: await listSlackInstallationBindings(db, {
|
|
143
|
+
accountId: grant.accountId,
|
|
144
|
+
workspaceId,
|
|
145
|
+
}),
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
107
150
|
app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
108
151
|
const workspaceId = c.req.param("workspaceId");
|
|
109
152
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
110
153
|
const payload = CreateConnectionRequest.parse(await c.req.json());
|
|
111
154
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
155
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
112
156
|
const key = requireEnvironmentEncryption(settings);
|
|
113
157
|
const subjectId = createConnectionSubjectId(payload, grant.subjectId);
|
|
114
158
|
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
115
159
|
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
116
160
|
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
161
|
+
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
117
162
|
const connection = await createConnection(db, {
|
|
118
163
|
accountId: grant.accountId,
|
|
119
164
|
workspaceId,
|
|
@@ -282,9 +327,30 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
282
327
|
);
|
|
283
328
|
});
|
|
284
329
|
|
|
285
|
-
app.
|
|
330
|
+
app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
|
|
286
331
|
assertIntegrationsEnabled();
|
|
287
|
-
const
|
|
332
|
+
const workspaceId = c.req.param("workspaceId");
|
|
333
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
334
|
+
const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
|
|
335
|
+
if (!parsed.success) {
|
|
336
|
+
throw new HTTPException(400, { message: "invalid Atlassian install request" });
|
|
337
|
+
}
|
|
338
|
+
return c.json(
|
|
339
|
+
AtlassianOAuthStartResponse.parse(
|
|
340
|
+
await startAtlassianOAuth(deps, {
|
|
341
|
+
accountId: grant.accountId,
|
|
342
|
+
workspaceId,
|
|
343
|
+
subjectId: grant.subjectId,
|
|
344
|
+
requestUrl: c.req.url,
|
|
345
|
+
payload: parsed.data,
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
app.get("/v1/integrations/atlassian/callback", async (c) => {
|
|
352
|
+
assertIntegrationsEnabled();
|
|
353
|
+
const result = await completeAtlassianOAuthCallback(deps, {
|
|
288
354
|
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
289
355
|
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
290
356
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
@@ -293,6 +359,82 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
293
359
|
return c.redirect(result.redirectTo, 302);
|
|
294
360
|
});
|
|
295
361
|
|
|
362
|
+
app.patch(
|
|
363
|
+
"/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/lifecycle",
|
|
364
|
+
async (c) => {
|
|
365
|
+
assertIntegrationsEnabled();
|
|
366
|
+
const workspaceId = c.req.param("workspaceId");
|
|
367
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
368
|
+
const parsed = AtlassianLifecycleActionRequest.safeParse(await c.req.json());
|
|
369
|
+
if (!parsed.success) {
|
|
370
|
+
throw new HTTPException(400, { message: "invalid Atlassian lifecycle request" });
|
|
371
|
+
}
|
|
372
|
+
return c.json(
|
|
373
|
+
ConnectionResponse.parse({
|
|
374
|
+
connection: await transitionAtlassianLifecycle(deps, {
|
|
375
|
+
workspaceId,
|
|
376
|
+
subjectId: grant.subjectId,
|
|
377
|
+
connectionId: c.req.param("connectionId"),
|
|
378
|
+
payload: parsed.data,
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
app.get("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/browse", async (c) => {
|
|
386
|
+
assertIntegrationsEnabled();
|
|
387
|
+
const workspaceId = c.req.param("workspaceId");
|
|
388
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
389
|
+
return c.json(
|
|
390
|
+
await browseAtlassianSources(deps, {
|
|
391
|
+
workspaceId,
|
|
392
|
+
subjectId: grant.subjectId,
|
|
393
|
+
connectionId: c.req.param("connectionId"),
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
app.post("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/source", async (c) => {
|
|
399
|
+
assertIntegrationsEnabled();
|
|
400
|
+
const workspaceId = c.req.param("workspaceId");
|
|
401
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
402
|
+
c,
|
|
403
|
+
deps,
|
|
404
|
+
workspaceId,
|
|
405
|
+
"connections:write",
|
|
406
|
+
);
|
|
407
|
+
const { grant } = authorization;
|
|
408
|
+
const connection = await saveAtlassianSources(deps, {
|
|
409
|
+
accountId: grant.accountId,
|
|
410
|
+
workspaceId,
|
|
411
|
+
subjectId: grant.subjectId,
|
|
412
|
+
grant,
|
|
413
|
+
connectionId: c.req.param("connectionId"),
|
|
414
|
+
payload: await c.req.json(),
|
|
415
|
+
canManageOrganizationDestination:
|
|
416
|
+
authorization.accountGrant?.permissions.includes("account:admin") === true,
|
|
417
|
+
canManageWorkspaceDestination: hasPermission(grant.permissions, "workspace:admin"),
|
|
418
|
+
canManagePersonalDestination:
|
|
419
|
+
authorization.contextIntegrity && authorization.authenticatedSubjectId === grant.subjectId,
|
|
420
|
+
});
|
|
421
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.get("/v1/integrations/google-drive/callback", async (c) => {
|
|
425
|
+
assertIntegrationsEnabled();
|
|
426
|
+
const input = {
|
|
427
|
+
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
428
|
+
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
429
|
+
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
430
|
+
requestUrl: c.req.url,
|
|
431
|
+
};
|
|
432
|
+
const result = isApiIntegrationProviderOAuthState(input.state, deps.settings)
|
|
433
|
+
? await completeApiIntegrationProviderOAuth(deps, input)
|
|
434
|
+
: await completeGoogleDriveOAuthCallback(deps, input);
|
|
435
|
+
return c.redirect(result.redirectTo, 302);
|
|
436
|
+
});
|
|
437
|
+
|
|
296
438
|
app.patch(
|
|
297
439
|
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
|
|
298
440
|
async (c) => {
|
|
@@ -350,6 +492,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
350
492
|
accountId: grant.accountId,
|
|
351
493
|
workspaceId,
|
|
352
494
|
subjectId: grant.subjectId,
|
|
495
|
+
grant,
|
|
353
496
|
connectionId: c.req.param("connectionId"),
|
|
354
497
|
payload: await c.req.json(),
|
|
355
498
|
canManageOrganizationDestination:
|
|
@@ -380,9 +523,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
380
523
|
|
|
381
524
|
app.patch("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
382
525
|
const workspaceId = c.req.param("workspaceId");
|
|
383
|
-
const
|
|
526
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
527
|
+
c,
|
|
528
|
+
deps,
|
|
529
|
+
workspaceId,
|
|
530
|
+
"connections:write",
|
|
531
|
+
);
|
|
532
|
+
const { grant } = authorization;
|
|
384
533
|
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
385
534
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
535
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
386
536
|
const existing = await getConnectionMetadata(
|
|
387
537
|
db,
|
|
388
538
|
workspaceId,
|
|
@@ -390,15 +540,85 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
390
540
|
grant.subjectId,
|
|
391
541
|
);
|
|
392
542
|
if (existing && isOpenGeniSlackBotConnection(existing)) {
|
|
393
|
-
|
|
394
|
-
|
|
543
|
+
const destination = ConnectorDocumentDestinationSelection.safeParse(
|
|
544
|
+
payload.metadata?.documentDestination,
|
|
545
|
+
);
|
|
546
|
+
const destinationOnlyUpdate =
|
|
547
|
+
payload.metadata !== undefined &&
|
|
548
|
+
Object.keys(payload.metadata).length === 1 &&
|
|
549
|
+
payload.providerDomain === undefined &&
|
|
550
|
+
payload.subjectId === undefined &&
|
|
551
|
+
payload.kind === undefined &&
|
|
552
|
+
payload.status === undefined &&
|
|
553
|
+
payload.credential === undefined &&
|
|
554
|
+
payload.grantedScopes === undefined &&
|
|
555
|
+
payload.expiresAt === undefined;
|
|
556
|
+
if (!destination.success || !destinationOnlyUpdate) {
|
|
557
|
+
throw new HTTPException(422, {
|
|
558
|
+
message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const destinationSelection = destination.data;
|
|
562
|
+
if (
|
|
563
|
+
destinationSelection.authorityKind === "organization" &&
|
|
564
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
565
|
+
) {
|
|
566
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
567
|
+
}
|
|
568
|
+
if (
|
|
569
|
+
destinationSelection.authorityKind === "workspace" &&
|
|
570
|
+
!hasPermission(grant.permissions, "workspace:admin")
|
|
571
|
+
) {
|
|
572
|
+
throw new HTTPException(403, { message: "missing permission: workspace:admin" });
|
|
573
|
+
}
|
|
574
|
+
if (
|
|
575
|
+
destinationSelection.authorityKind === "personal" &&
|
|
576
|
+
(!authorization.contextIntegrity ||
|
|
577
|
+
authorization.authenticatedSubjectId !== grant.subjectId)
|
|
578
|
+
) {
|
|
579
|
+
throw new HTTPException(403, {
|
|
580
|
+
message: "personal destination requires the exact actor",
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const metadata = {
|
|
584
|
+
...existing.metadata,
|
|
585
|
+
documentDestination: bindConnectorDocumentDestination(destinationSelection, {
|
|
586
|
+
accountId: grant.accountId,
|
|
587
|
+
workspaceId,
|
|
588
|
+
initiatingSubjectId: grant.subjectId,
|
|
589
|
+
}),
|
|
590
|
+
};
|
|
591
|
+
const connection = await updateSlackBotDocumentDestination(db, {
|
|
592
|
+
accountId: grant.accountId,
|
|
593
|
+
workspaceId,
|
|
594
|
+
connectionId: existing.id,
|
|
595
|
+
visibleToSubjectId: grant.subjectId,
|
|
596
|
+
expectedVersion: existing.version,
|
|
597
|
+
metadata,
|
|
598
|
+
updatedBySubjectId: grant.subjectId,
|
|
395
599
|
});
|
|
600
|
+
if (!connection) {
|
|
601
|
+
throw new HTTPException(409, {
|
|
602
|
+
message: "Slack bot connection changed; reload before saving its destination",
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
396
606
|
}
|
|
397
607
|
if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
|
|
398
608
|
throw new HTTPException(422, {
|
|
399
609
|
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
400
610
|
});
|
|
401
611
|
}
|
|
612
|
+
if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
|
|
613
|
+
throw new HTTPException(422, {
|
|
614
|
+
message: "use the dedicated Integration provider OAuth flow to update this connection",
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
|
|
618
|
+
throw new HTTPException(422, {
|
|
619
|
+
message: "use the dedicated Atlassian reconnect or source-selection flow",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
402
622
|
if (existing) {
|
|
403
623
|
const providerDomain = canonicalProviderDomain(
|
|
404
624
|
payload.providerDomain ?? existing.providerDomain,
|
|
@@ -406,6 +626,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
406
626
|
const kind = payload.kind ?? existing.kind;
|
|
407
627
|
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
408
628
|
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
629
|
+
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
409
630
|
}
|
|
410
631
|
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
411
632
|
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
@@ -467,8 +688,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
467
688
|
existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
468
689
|
existing.kind === "oauth2" &&
|
|
469
690
|
GoogleDriveConnectionMetadata.safeParse(existing.metadata).success;
|
|
691
|
+
const isAtlassian =
|
|
692
|
+
existing.subjectId === grant.subjectId &&
|
|
693
|
+
existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
|
|
694
|
+
existing.kind === "oauth2" &&
|
|
695
|
+
AtlassianConnectionMetadata.safeParse(existing.metadata).success;
|
|
696
|
+
const disconnectPayload = await c.req.json().catch(() => null);
|
|
470
697
|
const googleDriveDisconnect = isGoogleDrive
|
|
471
|
-
? GoogleDriveDisconnectRequest.safeParse(
|
|
698
|
+
? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
|
|
472
699
|
: null;
|
|
473
700
|
if (googleDriveDisconnect && !googleDriveDisconnect.success) {
|
|
474
701
|
throw new HTTPException(400, {
|
|
@@ -477,7 +704,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
477
704
|
"invalid Google Drive disconnect request",
|
|
478
705
|
});
|
|
479
706
|
}
|
|
480
|
-
|
|
707
|
+
const atlassianDisconnect = isAtlassian
|
|
708
|
+
? AtlassianDisconnectRequest.safeParse(disconnectPayload)
|
|
709
|
+
: null;
|
|
710
|
+
if (atlassianDisconnect && !atlassianDisconnect.success) {
|
|
711
|
+
throw new HTTPException(400, {
|
|
712
|
+
message:
|
|
713
|
+
atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian) {
|
|
481
717
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
482
718
|
}
|
|
483
719
|
const connection = isGoogleDrive
|
|
@@ -487,18 +723,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
487
723
|
connection: existing,
|
|
488
724
|
payload: googleDriveDisconnect!.data,
|
|
489
725
|
})
|
|
490
|
-
:
|
|
491
|
-
? await
|
|
492
|
-
accountId: grant.accountId,
|
|
726
|
+
: isAtlassian
|
|
727
|
+
? await disconnectAtlassian(deps, {
|
|
493
728
|
workspaceId,
|
|
494
729
|
subjectId: grant.subjectId,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
498
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
499
|
-
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
730
|
+
connection: existing,
|
|
731
|
+
payload: atlassianDisconnect!.data,
|
|
500
732
|
})
|
|
501
|
-
:
|
|
733
|
+
: isOpenGeniSlackBotConnection(existing)
|
|
734
|
+
? await revokeConnectionWithSlackBotSuccessAudit(db, {
|
|
735
|
+
accountId: grant.accountId,
|
|
736
|
+
workspaceId,
|
|
737
|
+
subjectId: grant.subjectId,
|
|
738
|
+
connectionId,
|
|
739
|
+
expectedVersion: existing.version,
|
|
740
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
741
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
742
|
+
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
743
|
+
})
|
|
744
|
+
: await revokeConnection(
|
|
745
|
+
db,
|
|
746
|
+
workspaceId,
|
|
747
|
+
connectionId,
|
|
748
|
+
grant.subjectId,
|
|
749
|
+
existing.version,
|
|
750
|
+
);
|
|
502
751
|
if (!connection) {
|
|
503
752
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
504
753
|
}
|
|
@@ -529,6 +778,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
529
778
|
return c.json(OAuthStartResponse.parse(result));
|
|
530
779
|
});
|
|
531
780
|
|
|
781
|
+
app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
|
|
782
|
+
assertIntegrationsEnabled();
|
|
783
|
+
const workspaceId = c.req.param("workspaceId");
|
|
784
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
785
|
+
const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
|
|
786
|
+
if (!parsed.success) {
|
|
787
|
+
throw new HTTPException(400, {
|
|
788
|
+
message: parsed.error.issues[0]?.message ?? "invalid Integration OAuth start request",
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return c.json(
|
|
792
|
+
OAuthStartResponse.parse(
|
|
793
|
+
await startApiIntegrationProviderOAuth(deps, {
|
|
794
|
+
accountId: grant.accountId,
|
|
795
|
+
workspaceId,
|
|
796
|
+
subjectId: grant.subjectId,
|
|
797
|
+
requestUrl: c.req.url,
|
|
798
|
+
payload: parsed.data,
|
|
799
|
+
}),
|
|
800
|
+
),
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
|
|
532
804
|
app.get("/v1/integrations/oauth/callback", async (c) => {
|
|
533
805
|
assertIntegrationsEnabled();
|
|
534
806
|
const result = await completeMcpOAuthCallback(
|
|
@@ -542,6 +814,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
542
814
|
return c.redirect(result.redirectTo, 302);
|
|
543
815
|
});
|
|
544
816
|
|
|
817
|
+
app.get("/v1/integrations/provider-oauth/callback", async (c) => {
|
|
818
|
+
assertIntegrationsEnabled();
|
|
819
|
+
const result = await completeApiIntegrationProviderOAuth(deps, {
|
|
820
|
+
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
821
|
+
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
822
|
+
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
823
|
+
requestUrl: c.req.url,
|
|
824
|
+
});
|
|
825
|
+
return c.redirect(result.redirectTo, 302);
|
|
826
|
+
});
|
|
827
|
+
|
|
545
828
|
app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
|
|
546
829
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
547
830
|
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
@@ -599,14 +882,9 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
599
882
|
"principal_validation",
|
|
600
883
|
);
|
|
601
884
|
}
|
|
602
|
-
const
|
|
603
|
-
requestedExisting
|
|
604
|
-
|
|
605
|
-
db,
|
|
606
|
-
input.state.workspaceId,
|
|
607
|
-
input.verified.metadata,
|
|
608
|
-
));
|
|
609
|
-
const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
|
|
885
|
+
const existingMetadata = requestedExisting
|
|
886
|
+
? openGeniSlackBotMetadata(requestedExisting.metadata)
|
|
887
|
+
: null;
|
|
610
888
|
if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
|
|
611
889
|
throw new SlackInstallCallbackError(
|
|
612
890
|
409,
|
|
@@ -628,83 +906,36 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
628
906
|
);
|
|
629
907
|
}
|
|
630
908
|
const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
updatedBySubjectId: input.state.subjectId,
|
|
658
|
-
},
|
|
659
|
-
})
|
|
660
|
-
: await createConnectionWithSlackBotSuccessAudit(db, {
|
|
661
|
-
...lifecycleAudit,
|
|
662
|
-
connection: {
|
|
663
|
-
accountId: input.state.accountId,
|
|
664
|
-
workspaceId: input.state.workspaceId,
|
|
665
|
-
subjectId: null,
|
|
666
|
-
providerDomain: "slack.com",
|
|
667
|
-
kind: "app_install",
|
|
668
|
-
credentialEncrypted,
|
|
669
|
-
grantedScopes: input.verified.grantedScopes,
|
|
670
|
-
expiresAt: null,
|
|
671
|
-
verifiedInstallAt,
|
|
672
|
-
verifiedInstallVersion: 1,
|
|
673
|
-
metadata: input.verified.metadata,
|
|
674
|
-
createdBySubjectId: input.state.subjectId,
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
if (!connection) {
|
|
678
|
-
throw new SlackInstallCallbackError(
|
|
679
|
-
409,
|
|
680
|
-
"connection_conflict",
|
|
681
|
-
"Slack bot connection changed during reinstall; start again",
|
|
682
|
-
"principal_validation",
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
return connection;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
async function findMatchingOpenGeniSlackBotConnection(
|
|
689
|
-
db: ApiRouteDeps["db"],
|
|
690
|
-
workspaceId: string,
|
|
691
|
-
verified: Awaited<ReturnType<typeof verifyOpenGeniSlackBotCredential>>["metadata"],
|
|
692
|
-
) {
|
|
693
|
-
const connections = await listConnectionsMetadata(db, workspaceId, null);
|
|
694
|
-
return (
|
|
695
|
-
connections.find((connection) => {
|
|
696
|
-
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
697
|
-
return (
|
|
698
|
-
connection.subjectId === null &&
|
|
699
|
-
connection.providerDomain === "slack.com" &&
|
|
700
|
-
connection.kind === "app_install" &&
|
|
701
|
-
metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE &&
|
|
702
|
-
metadata.slackTeamId === verified.slackTeamId &&
|
|
703
|
-
metadata.botId === verified.botId &&
|
|
704
|
-
metadata.botUserId === verified.botUserId
|
|
909
|
+
try {
|
|
910
|
+
return await persistSlackBotInstallationWithSuccessAudit(db, {
|
|
911
|
+
accountId: input.state.accountId,
|
|
912
|
+
workspaceId: input.state.workspaceId,
|
|
913
|
+
subjectId: input.state.subjectId,
|
|
914
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
915
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
916
|
+
slackTeamId: input.verified.metadata.slackTeamId,
|
|
917
|
+
...(input.state.connectionId
|
|
918
|
+
? {
|
|
919
|
+
requestedConnectionId: input.state.connectionId,
|
|
920
|
+
requestedConnectionVersion: input.state.connectionVersion,
|
|
921
|
+
}
|
|
922
|
+
: {}),
|
|
923
|
+
credentialEncrypted,
|
|
924
|
+
grantedScopes: input.verified.grantedScopes,
|
|
925
|
+
verifiedInstallAt,
|
|
926
|
+
metadata: input.verified.metadata,
|
|
927
|
+
});
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (error instanceof SlackInstallationBindingConflictError) {
|
|
930
|
+
throw new SlackInstallCallbackError(
|
|
931
|
+
409,
|
|
932
|
+
"connection_conflict",
|
|
933
|
+
error.message,
|
|
934
|
+
"principal_validation",
|
|
705
935
|
);
|
|
706
|
-
}
|
|
707
|
-
|
|
936
|
+
}
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
708
939
|
}
|
|
709
940
|
|
|
710
941
|
function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
|
|
@@ -881,6 +1112,21 @@ function assertNotDirectGoogleDriveOAuth(
|
|
|
881
1112
|
}
|
|
882
1113
|
}
|
|
883
1114
|
|
|
1115
|
+
function assertNotDirectAtlassianOAuth(
|
|
1116
|
+
providerDomain: string,
|
|
1117
|
+
kind: string,
|
|
1118
|
+
metadata: Record<string, unknown> | undefined,
|
|
1119
|
+
): void {
|
|
1120
|
+
if (
|
|
1121
|
+
(providerDomain === ATLASSIAN_PROVIDER_DOMAIN && kind === "oauth2") ||
|
|
1122
|
+
AtlassianConnectionMetadata.safeParse(metadata).success
|
|
1123
|
+
) {
|
|
1124
|
+
throw new HTTPException(422, {
|
|
1125
|
+
message: "Atlassian credentials must use the dedicated OAuth connection flow",
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
884
1130
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
885
1131
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
886
1132
|
throw new HTTPException(422, {
|
|
@@ -889,6 +1135,16 @@ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | u
|
|
|
889
1135
|
}
|
|
890
1136
|
}
|
|
891
1137
|
|
|
1138
|
+
function assertNotReservedApiIntegrationOAuthMetadata(
|
|
1139
|
+
metadata: Record<string, unknown> | undefined,
|
|
1140
|
+
): void {
|
|
1141
|
+
if (metadata?.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
|
|
1142
|
+
throw new HTTPException(422, {
|
|
1143
|
+
message: "API Integration OAuth metadata is reserved for the dedicated provider flow",
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
892
1148
|
function writableSubjectId(
|
|
893
1149
|
requested: string | null | undefined,
|
|
894
1150
|
grantSubjectId: string,
|
package/src/routes/documents.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
getKnowledgeMemory,
|
|
22
22
|
listKnowledgeMemories,
|
|
23
23
|
updateKnowledgeMemory,
|
|
24
|
-
saveWorkspaceMemory,
|
|
25
24
|
searchWorkspaceMemories,
|
|
26
25
|
} from "@opengeni/db";
|
|
27
26
|
import {
|
|
@@ -40,7 +39,11 @@ import {
|
|
|
40
39
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
41
40
|
import type { Context, Hono } from "hono";
|
|
42
41
|
import { HTTPException } from "hono/http-exception";
|
|
43
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
requireAccessGrant,
|
|
44
|
+
requireAccessGrantAuthorization,
|
|
45
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
46
|
+
} from "@opengeni/core";
|
|
44
47
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
45
48
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
46
49
|
import { buildDocumentsMcpServer } from "../mcp/documents";
|
|
@@ -561,12 +564,17 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
561
564
|
throw new HTTPException(400, { message: "invalid knowledge memory request" });
|
|
562
565
|
}
|
|
563
566
|
const payload = parsedBody.data;
|
|
567
|
+
if (payload.status !== "active" && payload.slackPublication) {
|
|
568
|
+
throw new HTTPException(400, {
|
|
569
|
+
message: "Slack publication is available only for active Workspace Memory writes",
|
|
570
|
+
});
|
|
571
|
+
}
|
|
564
572
|
// status `active` (the default) is a memory write → route through the single
|
|
565
573
|
// gate (sanitize + embed + dedup). Explicit proposed/approved/rejected keeps
|
|
566
574
|
// the legacy curated create.
|
|
567
575
|
if (payload.status === "active") {
|
|
568
576
|
try {
|
|
569
|
-
const result = await
|
|
577
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
570
578
|
db,
|
|
571
579
|
{
|
|
572
580
|
accountId: grant.accountId,
|
|
@@ -579,6 +587,17 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
579
587
|
metadata: payload.metadata,
|
|
580
588
|
origin: "human",
|
|
581
589
|
},
|
|
590
|
+
payload.slackPublication
|
|
591
|
+
? {
|
|
592
|
+
distribution: payload.slackPublication,
|
|
593
|
+
actor: {
|
|
594
|
+
kind: "human",
|
|
595
|
+
subjectId: grant.subjectId,
|
|
596
|
+
initiatingHumanSubjectId: grant.subjectId,
|
|
597
|
+
},
|
|
598
|
+
ownerLabel: grant.subjectLabel ?? null,
|
|
599
|
+
}
|
|
600
|
+
: null,
|
|
582
601
|
getDocumentServices().embedder,
|
|
583
602
|
);
|
|
584
603
|
return c.json(KnowledgeMemory.parse(result.memory), 201);
|