@opengeni/api-router 0.23.1 → 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/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
- 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/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- 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 +22 -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 +8 -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/scheduled-task-view.d.ts +168 -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/channel-a.d.ts +55 -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 +274 -37
- 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/index.ts +119 -20
- 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 +123 -33
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +487 -27
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +32 -1
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +923 -145
- package/src/memory-slack-delivery.ts +209 -0
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +284 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-features.ts +258 -0
- 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 +67 -40
- package/src/routes/sessions.ts +421 -245
- package/src/routes/skills.ts +174 -0
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/channel-a.ts +723 -116
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +29 -9
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
|
@@ -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,
|
|
@@ -34,17 +45,18 @@ import {
|
|
|
34
45
|
import {
|
|
35
46
|
consumeIntegrationOAuthStateNonce,
|
|
36
47
|
createConnection,
|
|
37
|
-
createConnectionWithSlackBotSuccessAudit,
|
|
38
48
|
encryptEnvironmentValue,
|
|
39
49
|
getConnectionMetadata,
|
|
40
50
|
getWorkspaceGrant,
|
|
41
51
|
listConnectionsMetadata,
|
|
52
|
+
listSlackInstallationBindings,
|
|
53
|
+
persistSlackBotInstallationWithSuccessAudit,
|
|
42
54
|
recordSlackBotInstallCallbackFailure,
|
|
43
55
|
revokeConnection,
|
|
44
56
|
revokeConnectionWithSlackBotSuccessAudit,
|
|
45
57
|
SlackBotLifecycleSuccessAuditError,
|
|
58
|
+
SlackInstallationBindingConflictError,
|
|
46
59
|
updateConnection,
|
|
47
|
-
updateConnectionWithSlackBotSuccessAudit,
|
|
48
60
|
updateSlackBotDocumentDestination,
|
|
49
61
|
type SlackBotInstallCallbackFailureReason,
|
|
50
62
|
type SlackBotInstallCallbackFailureStage,
|
|
@@ -60,6 +72,19 @@ import {
|
|
|
60
72
|
startGoogleDriveOAuth,
|
|
61
73
|
transitionGoogleDriveLifecycle,
|
|
62
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";
|
|
63
88
|
import {
|
|
64
89
|
completeMcpOAuthCallback,
|
|
65
90
|
integrationBaseUrl,
|
|
@@ -109,16 +134,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
109
134
|
);
|
|
110
135
|
});
|
|
111
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
|
+
|
|
112
150
|
app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
113
151
|
const workspaceId = c.req.param("workspaceId");
|
|
114
152
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
115
153
|
const payload = CreateConnectionRequest.parse(await c.req.json());
|
|
116
154
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
155
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
117
156
|
const key = requireEnvironmentEncryption(settings);
|
|
118
157
|
const subjectId = createConnectionSubjectId(payload, grant.subjectId);
|
|
119
158
|
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
120
159
|
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
121
160
|
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
161
|
+
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
122
162
|
const connection = await createConnection(db, {
|
|
123
163
|
accountId: grant.accountId,
|
|
124
164
|
workspaceId,
|
|
@@ -287,9 +327,30 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
287
327
|
);
|
|
288
328
|
});
|
|
289
329
|
|
|
290
|
-
app.
|
|
330
|
+
app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
|
|
331
|
+
assertIntegrationsEnabled();
|
|
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) => {
|
|
291
352
|
assertIntegrationsEnabled();
|
|
292
|
-
const result = await
|
|
353
|
+
const result = await completeAtlassianOAuthCallback(deps, {
|
|
293
354
|
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
294
355
|
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
295
356
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
@@ -298,6 +359,82 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
298
359
|
return c.redirect(result.redirectTo, 302);
|
|
299
360
|
});
|
|
300
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
|
+
|
|
301
438
|
app.patch(
|
|
302
439
|
"/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
|
|
303
440
|
async (c) => {
|
|
@@ -355,6 +492,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
355
492
|
accountId: grant.accountId,
|
|
356
493
|
workspaceId,
|
|
357
494
|
subjectId: grant.subjectId,
|
|
495
|
+
grant,
|
|
358
496
|
connectionId: c.req.param("connectionId"),
|
|
359
497
|
payload: await c.req.json(),
|
|
360
498
|
canManageOrganizationDestination:
|
|
@@ -394,6 +532,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
394
532
|
const { grant } = authorization;
|
|
395
533
|
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
396
534
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
535
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
397
536
|
const existing = await getConnectionMetadata(
|
|
398
537
|
db,
|
|
399
538
|
workspaceId,
|
|
@@ -470,6 +609,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
470
609
|
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
471
610
|
});
|
|
472
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
|
+
}
|
|
473
622
|
if (existing) {
|
|
474
623
|
const providerDomain = canonicalProviderDomain(
|
|
475
624
|
payload.providerDomain ?? existing.providerDomain,
|
|
@@ -477,6 +626,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
477
626
|
const kind = payload.kind ?? existing.kind;
|
|
478
627
|
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
479
628
|
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
629
|
+
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
480
630
|
}
|
|
481
631
|
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
482
632
|
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
@@ -538,8 +688,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
538
688
|
existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
539
689
|
existing.kind === "oauth2" &&
|
|
540
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);
|
|
541
697
|
const googleDriveDisconnect = isGoogleDrive
|
|
542
|
-
? GoogleDriveDisconnectRequest.safeParse(
|
|
698
|
+
? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
|
|
543
699
|
: null;
|
|
544
700
|
if (googleDriveDisconnect && !googleDriveDisconnect.success) {
|
|
545
701
|
throw new HTTPException(400, {
|
|
@@ -548,7 +704,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
548
704
|
"invalid Google Drive disconnect request",
|
|
549
705
|
});
|
|
550
706
|
}
|
|
551
|
-
|
|
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) {
|
|
552
717
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
553
718
|
}
|
|
554
719
|
const connection = isGoogleDrive
|
|
@@ -558,18 +723,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
558
723
|
connection: existing,
|
|
559
724
|
payload: googleDriveDisconnect!.data,
|
|
560
725
|
})
|
|
561
|
-
:
|
|
562
|
-
? await
|
|
563
|
-
accountId: grant.accountId,
|
|
726
|
+
: isAtlassian
|
|
727
|
+
? await disconnectAtlassian(deps, {
|
|
564
728
|
workspaceId,
|
|
565
729
|
subjectId: grant.subjectId,
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
569
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
570
|
-
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
730
|
+
connection: existing,
|
|
731
|
+
payload: atlassianDisconnect!.data,
|
|
571
732
|
})
|
|
572
|
-
:
|
|
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
|
+
);
|
|
573
751
|
if (!connection) {
|
|
574
752
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
575
753
|
}
|
|
@@ -600,6 +778,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
600
778
|
return c.json(OAuthStartResponse.parse(result));
|
|
601
779
|
});
|
|
602
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
|
+
|
|
603
804
|
app.get("/v1/integrations/oauth/callback", async (c) => {
|
|
604
805
|
assertIntegrationsEnabled();
|
|
605
806
|
const result = await completeMcpOAuthCallback(
|
|
@@ -613,6 +814,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
613
814
|
return c.redirect(result.redirectTo, 302);
|
|
614
815
|
});
|
|
615
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
|
+
|
|
616
828
|
app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
|
|
617
829
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
618
830
|
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
@@ -670,14 +882,9 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
670
882
|
"principal_validation",
|
|
671
883
|
);
|
|
672
884
|
}
|
|
673
|
-
const
|
|
674
|
-
requestedExisting
|
|
675
|
-
|
|
676
|
-
db,
|
|
677
|
-
input.state.workspaceId,
|
|
678
|
-
input.verified.metadata,
|
|
679
|
-
));
|
|
680
|
-
const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
|
|
885
|
+
const existingMetadata = requestedExisting
|
|
886
|
+
? openGeniSlackBotMetadata(requestedExisting.metadata)
|
|
887
|
+
: null;
|
|
681
888
|
if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
|
|
682
889
|
throw new SlackInstallCallbackError(
|
|
683
890
|
409,
|
|
@@ -699,83 +906,36 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
699
906
|
);
|
|
700
907
|
}
|
|
701
908
|
const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
updatedBySubjectId: input.state.subjectId,
|
|
729
|
-
},
|
|
730
|
-
})
|
|
731
|
-
: await createConnectionWithSlackBotSuccessAudit(db, {
|
|
732
|
-
...lifecycleAudit,
|
|
733
|
-
connection: {
|
|
734
|
-
accountId: input.state.accountId,
|
|
735
|
-
workspaceId: input.state.workspaceId,
|
|
736
|
-
subjectId: null,
|
|
737
|
-
providerDomain: "slack.com",
|
|
738
|
-
kind: "app_install",
|
|
739
|
-
credentialEncrypted,
|
|
740
|
-
grantedScopes: input.verified.grantedScopes,
|
|
741
|
-
expiresAt: null,
|
|
742
|
-
verifiedInstallAt,
|
|
743
|
-
verifiedInstallVersion: 1,
|
|
744
|
-
metadata: input.verified.metadata,
|
|
745
|
-
createdBySubjectId: input.state.subjectId,
|
|
746
|
-
},
|
|
747
|
-
});
|
|
748
|
-
if (!connection) {
|
|
749
|
-
throw new SlackInstallCallbackError(
|
|
750
|
-
409,
|
|
751
|
-
"connection_conflict",
|
|
752
|
-
"Slack bot connection changed during reinstall; start again",
|
|
753
|
-
"principal_validation",
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
return connection;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
async function findMatchingOpenGeniSlackBotConnection(
|
|
760
|
-
db: ApiRouteDeps["db"],
|
|
761
|
-
workspaceId: string,
|
|
762
|
-
verified: Awaited<ReturnType<typeof verifyOpenGeniSlackBotCredential>>["metadata"],
|
|
763
|
-
) {
|
|
764
|
-
const connections = await listConnectionsMetadata(db, workspaceId, null);
|
|
765
|
-
return (
|
|
766
|
-
connections.find((connection) => {
|
|
767
|
-
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
768
|
-
return (
|
|
769
|
-
connection.subjectId === null &&
|
|
770
|
-
connection.providerDomain === "slack.com" &&
|
|
771
|
-
connection.kind === "app_install" &&
|
|
772
|
-
metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE &&
|
|
773
|
-
metadata.slackTeamId === verified.slackTeamId &&
|
|
774
|
-
metadata.botId === verified.botId &&
|
|
775
|
-
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",
|
|
776
935
|
);
|
|
777
|
-
}
|
|
778
|
-
|
|
936
|
+
}
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
779
939
|
}
|
|
780
940
|
|
|
781
941
|
function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
|
|
@@ -952,6 +1112,21 @@ function assertNotDirectGoogleDriveOAuth(
|
|
|
952
1112
|
}
|
|
953
1113
|
}
|
|
954
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
|
+
|
|
955
1130
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
956
1131
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
957
1132
|
throw new HTTPException(422, {
|
|
@@ -960,6 +1135,16 @@ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | u
|
|
|
960
1135
|
}
|
|
961
1136
|
}
|
|
962
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
|
+
|
|
963
1148
|
function writableSubjectId(
|
|
964
1149
|
requested: string | null | undefined,
|
|
965
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);
|