@opengeni/api-router 2.11.3 → 2.12.3-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-5X3XM6CE.js → chunk-4AQMKVQM.js} +17727 -14427
- package/dist/chunk-4AQMKVQM.js.map +1 -0
- package/dist/connection-ownership.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +4 -0
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +7 -0
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +153 -0
- package/dist/integrations/google-drive.d.ts +4 -0
- package/dist/integrations/oauth-client.d.ts +4 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github.d.ts +2 -0
- package/dist/integrations/provider-oauth.d.ts +11 -1
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/social-oauth.d.ts +2 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/host-mcp-bindings.d.ts +5 -0
- package/dist/sandbox/rematerialize.d.ts +1 -1
- package/dist/workspace-tool-gateway.d.ts +1 -0
- package/package.json +19 -19
- package/src/app.ts +22 -2
- package/src/connection-ownership.ts +14 -1
- package/src/index.ts +2 -0
- package/src/integrations/atlassian.ts +143 -39
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +222 -40
- package/src/integrations/github-app-connect.ts +389 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +97 -0
- package/src/integrations/google-drive.ts +152 -47
- package/src/integrations/oauth-client.ts +180 -84
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github.ts +146 -52
- package/src/integrations/provider-oauth.ts +132 -21
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/social-oauth.ts +142 -1
- package/src/mcp/documents.ts +1 -1
- package/src/mcp/server.ts +64 -17
- package/src/routes/api-integrations.ts +74 -64
- package/src/routes/connect.ts +1869 -0
- package/src/routes/connections.ts +263 -221
- package/src/routes/documents.ts +2 -2
- package/src/routes/external-identity-links.ts +200 -0
- package/src/routes/github.ts +39 -64
- package/src/routes/host-mcp-bindings.ts +122 -0
- package/src/routes/organization-memberships.ts +23 -0
- package/src/routes/organization-sessions.ts +2 -2
- package/src/routes/personal-github.ts +8 -1
- package/src/routes/pr-review-github.ts +30 -3
- package/src/routes/scheduled-tasks.ts +29 -4
- package/src/routes/sessions.ts +61 -32
- package/src/routes/social.ts +5 -1
- package/src/routes/supergrok.ts +90 -3
- package/src/routes/workspaces.ts +16 -0
- package/src/sandbox/rematerialize.ts +33 -1
- package/src/workspace-tool-gateway.ts +136 -24
- package/dist/chunk-5X3XM6CE.js.map +0 -1
|
@@ -0,0 +1,1869 @@
|
|
|
1
|
+
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import {
|
|
5
|
+
AdvanceConnectRequest,
|
|
6
|
+
BeginConnectRequest,
|
|
7
|
+
ConnectOperationRequest,
|
|
8
|
+
ConnectProvider,
|
|
9
|
+
ConnectAccount,
|
|
10
|
+
} from "@opengeni/contracts/connect";
|
|
11
|
+
import {
|
|
12
|
+
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
13
|
+
InstallApiIntegrationRequest,
|
|
14
|
+
stableJson,
|
|
15
|
+
OPENGENI_PERSONAL_SLACK_MCP_URL,
|
|
16
|
+
OPENGENI_PR_REVIEW_PACK_ID,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
import {
|
|
19
|
+
CORE_INTEGRATION_DEFINITIONS,
|
|
20
|
+
createPinnedIntegrationTransport,
|
|
21
|
+
} from "@opengeni/capabilities";
|
|
22
|
+
import {
|
|
23
|
+
beginConnectAttempt,
|
|
24
|
+
getConnectAttempt,
|
|
25
|
+
listPendingConnectAttempts,
|
|
26
|
+
withWorkspaceSubjectRls,
|
|
27
|
+
installApiIntegration,
|
|
28
|
+
listConnectionsMetadata,
|
|
29
|
+
getConnectionMetadata,
|
|
30
|
+
createConnection,
|
|
31
|
+
updateConnection,
|
|
32
|
+
encryptEnvironmentValue,
|
|
33
|
+
normalizedHostCredentialHeaders,
|
|
34
|
+
listGitHubInstallationAccessForWorkspace,
|
|
35
|
+
getPackInstallation,
|
|
36
|
+
listPrReviewAppRegistrations,
|
|
37
|
+
listSocialConnections,
|
|
38
|
+
getSocialConnection,
|
|
39
|
+
} from "@opengeni/db";
|
|
40
|
+
import {
|
|
41
|
+
externalActorContinuationForAuthorization,
|
|
42
|
+
requireAccessGrantAuthorization,
|
|
43
|
+
hasPermission,
|
|
44
|
+
requireEnvironmentEncryption,
|
|
45
|
+
isFikenConnection,
|
|
46
|
+
isOpenGeniSlackBotConnection,
|
|
47
|
+
prepareCapabilityEnable,
|
|
48
|
+
buildCapabilityCatalog,
|
|
49
|
+
type ApiRouteDeps,
|
|
50
|
+
} from "@opengeni/core";
|
|
51
|
+
import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
|
|
52
|
+
import { requireConnectOwnerAuthority } from "../integrations/connect-authority";
|
|
53
|
+
import { prepareFikenTokenInstall, startFikenOAuth } from "../integrations/fiken";
|
|
54
|
+
import { startAtlassianOAuth } from "../integrations/atlassian";
|
|
55
|
+
import { startSlackBotInstall } from "../integrations/slack-install";
|
|
56
|
+
import { startPersonalGitHubOAuth } from "../integrations/personal-github";
|
|
57
|
+
import { socialOAuthClientFor, startSocialOAuth } from "../integrations/social-oauth";
|
|
58
|
+
import {
|
|
59
|
+
githubAppConnectNavigation,
|
|
60
|
+
prepareGitHubAppConnectAction,
|
|
61
|
+
} from "../integrations/github-app-connect";
|
|
62
|
+
import { githubAppMissingSettings, prReviewGitHubAppMissingSettings } from "@opengeni/github";
|
|
63
|
+
import { requireGitHubLensConnect } from "../integrations/github-lens-connect";
|
|
64
|
+
import { isPersonalGitHubConnection } from "@opengeni/contracts/personal-github";
|
|
65
|
+
import { startGoogleDriveOAuth } from "../integrations/google-drive";
|
|
66
|
+
import {
|
|
67
|
+
GOOGLE_DRIVE_CREDENTIAL_ROLE,
|
|
68
|
+
googleDriveScopesAllowCapability,
|
|
69
|
+
} from "@opengeni/contracts/google-drive";
|
|
70
|
+
import { ATLASSIAN_CREDENTIAL_ROLE } from "@opengeni/contracts/atlassian";
|
|
71
|
+
import {
|
|
72
|
+
curatedOAuthReadiness,
|
|
73
|
+
startApiIntegrationProviderOAuth,
|
|
74
|
+
} from "../integrations/provider-oauth";
|
|
75
|
+
import { resolveForRoute, validatedIntegrationInstallInput } from "./api-integrations";
|
|
76
|
+
import { executeConnectOperation } from "@opengeni/core";
|
|
77
|
+
import { startMcpOAuth, requireIntegrationsStateSecret } from "../integrations/oauth-client";
|
|
78
|
+
import { z } from "zod";
|
|
79
|
+
import {
|
|
80
|
+
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
81
|
+
VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
82
|
+
} from "@opengeni/config";
|
|
83
|
+
|
|
84
|
+
/** Shared durable setup entry. Provider completion is distinct from
|
|
85
|
+
* subsequent preview/install; never report an OAuth token as a ready integration. */
|
|
86
|
+
export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
87
|
+
const canReadAttempt = (permissions: Parameters<typeof hasPermission>[0], providerId: string) =>
|
|
88
|
+
["x", "reddit", "mcp-install"].includes(providerId)
|
|
89
|
+
? hasPermission(permissions, "workspace:read")
|
|
90
|
+
: providerId === "github-app"
|
|
91
|
+
? hasPermission(permissions, "github:use") || hasPermission(permissions, "github:manage")
|
|
92
|
+
: providerId === "github-lens"
|
|
93
|
+
? hasPermission(permissions, "workspace:read")
|
|
94
|
+
: hasPermission(permissions, "connections:read");
|
|
95
|
+
app.get("/v1/workspaces/:workspaceId/connect/catalog", async (c) => {
|
|
96
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
97
|
+
c,
|
|
98
|
+
deps,
|
|
99
|
+
c.req.param("workspaceId"),
|
|
100
|
+
"workspace:read",
|
|
101
|
+
);
|
|
102
|
+
const external = authorization.contextIntegrity;
|
|
103
|
+
const canWrite = hasPermission(authorization.grant.permissions, "connections:write");
|
|
104
|
+
const personal = isPersonalConnectionOwnerPrincipal(authorization);
|
|
105
|
+
const lensPack = await getPackInstallation(
|
|
106
|
+
deps.db,
|
|
107
|
+
authorization.grant.workspaceId,
|
|
108
|
+
OPENGENI_PR_REVIEW_PACK_ID,
|
|
109
|
+
);
|
|
110
|
+
let mcpConfigured = false;
|
|
111
|
+
let credentialConfigured = false;
|
|
112
|
+
try {
|
|
113
|
+
requireEnvironmentEncryption(deps.settings);
|
|
114
|
+
credentialConfigured = deps.settings.integrationsEnabled;
|
|
115
|
+
requireIntegrationsStateSecret(deps.settings);
|
|
116
|
+
mcpConfigured = deps.settings.integrationsEnabled;
|
|
117
|
+
} catch {
|
|
118
|
+
/* Missing operator configuration is catalog readiness, not actor authority. */
|
|
119
|
+
}
|
|
120
|
+
return c.json([
|
|
121
|
+
...(["x", "reddit"] as const).map((providerId) => {
|
|
122
|
+
let configured = mcpConfigured;
|
|
123
|
+
try {
|
|
124
|
+
socialOAuthClientFor(deps.settings, providerId);
|
|
125
|
+
} catch {
|
|
126
|
+
configured = false;
|
|
127
|
+
}
|
|
128
|
+
const ownership = [
|
|
129
|
+
...(hasPermission(authorization.grant.permissions, "workspace:admin")
|
|
130
|
+
? ["workspace" as const]
|
|
131
|
+
: []),
|
|
132
|
+
...(personal ? ["personal" as const] : []),
|
|
133
|
+
];
|
|
134
|
+
return ConnectProvider.parse({
|
|
135
|
+
id: providerId,
|
|
136
|
+
label: providerId === "x" ? "X" : "Reddit",
|
|
137
|
+
family: "social",
|
|
138
|
+
readiness:
|
|
139
|
+
!external || !ownership.length
|
|
140
|
+
? "unsupported"
|
|
141
|
+
: configured
|
|
142
|
+
? "available"
|
|
143
|
+
: "needs_configuration",
|
|
144
|
+
ownership,
|
|
145
|
+
setup: ["oauth"],
|
|
146
|
+
});
|
|
147
|
+
}),
|
|
148
|
+
ConnectProvider.parse({
|
|
149
|
+
id: "mcp-install",
|
|
150
|
+
label: "Install MCP capability",
|
|
151
|
+
family: "mcp",
|
|
152
|
+
readiness:
|
|
153
|
+
!external || !hasPermission(authorization.grant.permissions, "capabilities:manage")
|
|
154
|
+
? "unsupported"
|
|
155
|
+
: deps.settings.integrationsEnabled
|
|
156
|
+
? "available"
|
|
157
|
+
: "needs_configuration",
|
|
158
|
+
ownership: ["workspace"],
|
|
159
|
+
setup: ["installation"],
|
|
160
|
+
}),
|
|
161
|
+
...CORE_INTEGRATION_DEFINITIONS.map((definition) => {
|
|
162
|
+
const readiness = curatedOAuthReadiness(deps.settings, definition);
|
|
163
|
+
return ConnectProvider.parse({
|
|
164
|
+
id: definition.id,
|
|
165
|
+
label: definition.name,
|
|
166
|
+
family: definition.provider.id,
|
|
167
|
+
readiness:
|
|
168
|
+
!external || !canWrite
|
|
169
|
+
? "unsupported"
|
|
170
|
+
: readiness.configured
|
|
171
|
+
? "available"
|
|
172
|
+
: "needs_configuration",
|
|
173
|
+
...(!external
|
|
174
|
+
? { reason: "A verified user is required by this Connect adapter" }
|
|
175
|
+
: !canWrite
|
|
176
|
+
? { reason: "connections:write permission required" }
|
|
177
|
+
: !readiness.configured
|
|
178
|
+
? { reason: "Deployment OAuth or encryption configuration is incomplete" }
|
|
179
|
+
: {}),
|
|
180
|
+
ownership: readiness.ownership.filter((owner) => owner !== "personal" || personal),
|
|
181
|
+
setup: ["oauth"],
|
|
182
|
+
});
|
|
183
|
+
}),
|
|
184
|
+
...(["openapi", "graphql"] as const).map((protocol) =>
|
|
185
|
+
ConnectProvider.parse({
|
|
186
|
+
id: protocol,
|
|
187
|
+
label: protocol === "openapi" ? "OpenAPI service" : "GraphQL service",
|
|
188
|
+
family: protocol,
|
|
189
|
+
readiness:
|
|
190
|
+
!external || !canWrite
|
|
191
|
+
? "unsupported"
|
|
192
|
+
: deps.settings.integrationsEnabled
|
|
193
|
+
? "available"
|
|
194
|
+
: "needs_configuration",
|
|
195
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
196
|
+
setup: [protocol, "installation"],
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
ConnectProvider.parse({
|
|
200
|
+
id: "atlassian",
|
|
201
|
+
label: "Atlassian (Jira and Confluence)",
|
|
202
|
+
family: "atlassian",
|
|
203
|
+
readiness:
|
|
204
|
+
!external || !canWrite || !personal
|
|
205
|
+
? "unsupported"
|
|
206
|
+
: mcpConfigured &&
|
|
207
|
+
deps.settings.atlassianClientId?.trim() &&
|
|
208
|
+
deps.settings.atlassianClientSecret?.trim()
|
|
209
|
+
? "available"
|
|
210
|
+
: "needs_configuration",
|
|
211
|
+
ownership: personal ? ["personal"] : [],
|
|
212
|
+
setup: ["oauth"],
|
|
213
|
+
}),
|
|
214
|
+
ConnectProvider.parse({
|
|
215
|
+
id: "slack-bot",
|
|
216
|
+
label: "Slack workspace bot",
|
|
217
|
+
family: "slack",
|
|
218
|
+
readiness:
|
|
219
|
+
!external || !canWrite
|
|
220
|
+
? "unsupported"
|
|
221
|
+
: mcpConfigured &&
|
|
222
|
+
deps.settings.slackClientId?.trim() &&
|
|
223
|
+
deps.settings.slackClientSecret?.trim() &&
|
|
224
|
+
deps.settings.slackSigningSecret?.trim()
|
|
225
|
+
? "available"
|
|
226
|
+
: "needs_configuration",
|
|
227
|
+
ownership: ["workspace"],
|
|
228
|
+
setup: ["oauth"],
|
|
229
|
+
}),
|
|
230
|
+
ConnectProvider.parse({
|
|
231
|
+
id: "slack-personal",
|
|
232
|
+
label: "My Slack account",
|
|
233
|
+
family: "slack",
|
|
234
|
+
readiness:
|
|
235
|
+
!external || !canWrite || !personal
|
|
236
|
+
? "unsupported"
|
|
237
|
+
: mcpConfigured &&
|
|
238
|
+
deps.settings.slackClientId?.trim() &&
|
|
239
|
+
deps.settings.slackClientSecret?.trim()
|
|
240
|
+
? "available"
|
|
241
|
+
: "needs_configuration",
|
|
242
|
+
ownership: personal ? ["personal"] : [],
|
|
243
|
+
setup: ["oauth"],
|
|
244
|
+
}),
|
|
245
|
+
ConnectProvider.parse({
|
|
246
|
+
id: "github-personal",
|
|
247
|
+
label: "My GitHub account",
|
|
248
|
+
family: "github",
|
|
249
|
+
readiness:
|
|
250
|
+
!external || !canWrite || !personal
|
|
251
|
+
? "unsupported"
|
|
252
|
+
: mcpConfigured &&
|
|
253
|
+
deps.settings.githubPersonalOauthEnabled &&
|
|
254
|
+
deps.settings.githubPersonalOauthClientId?.trim() &&
|
|
255
|
+
deps.settings.githubPersonalOauthClientSecret?.trim()
|
|
256
|
+
? "available"
|
|
257
|
+
: "needs_configuration",
|
|
258
|
+
ownership: personal ? ["personal"] : [],
|
|
259
|
+
setup: ["oauth"],
|
|
260
|
+
}),
|
|
261
|
+
ConnectProvider.parse({
|
|
262
|
+
id: "github-app",
|
|
263
|
+
label: "GitHub App installation",
|
|
264
|
+
family: "github",
|
|
265
|
+
readiness:
|
|
266
|
+
!external || !hasPermission(authorization.grant.permissions, "github:manage")
|
|
267
|
+
? "unsupported"
|
|
268
|
+
: githubAppMissingSettings(deps.settings).length === 0 &&
|
|
269
|
+
deps.settings.githubAppSlug?.trim()
|
|
270
|
+
? "available"
|
|
271
|
+
: "needs_configuration",
|
|
272
|
+
ownership: ["workspace"],
|
|
273
|
+
setup: ["oauth", "installation"],
|
|
274
|
+
}),
|
|
275
|
+
ConnectProvider.parse({
|
|
276
|
+
id: "github-lens",
|
|
277
|
+
label: "OpenGeni Lens PR review",
|
|
278
|
+
family: "github-lens",
|
|
279
|
+
readiness:
|
|
280
|
+
!external ||
|
|
281
|
+
!hasPermission(authorization.grant.permissions, "workspace:admin") ||
|
|
282
|
+
!hasPermission(authorization.grant.permissions, "secrets:write")
|
|
283
|
+
? "unsupported"
|
|
284
|
+
: credentialConfigured &&
|
|
285
|
+
lensPack?.status === "active" &&
|
|
286
|
+
deps.settings.sandboxBackend !== "selfhosted" &&
|
|
287
|
+
prReviewGitHubAppMissingSettings(deps.settings).length === 0
|
|
288
|
+
? "available"
|
|
289
|
+
: "needs_configuration",
|
|
290
|
+
ownership: ["workspace"],
|
|
291
|
+
setup: ["oauth", "installation"],
|
|
292
|
+
}),
|
|
293
|
+
...(["google-drive-knowledge", "google-drive-publish"] as const).map((id) =>
|
|
294
|
+
ConnectProvider.parse({
|
|
295
|
+
id,
|
|
296
|
+
label:
|
|
297
|
+
id === "google-drive-publish"
|
|
298
|
+
? "Google Drive publishing"
|
|
299
|
+
: "Google Drive knowledge sources",
|
|
300
|
+
family: "google-drive",
|
|
301
|
+
readiness:
|
|
302
|
+
!external || !canWrite || !personal
|
|
303
|
+
? "unsupported"
|
|
304
|
+
: mcpConfigured &&
|
|
305
|
+
deps.settings.googleDriveClientId?.trim() &&
|
|
306
|
+
deps.settings.googleDriveClientSecret?.trim()
|
|
307
|
+
? "available"
|
|
308
|
+
: "needs_configuration",
|
|
309
|
+
ownership: personal ? ["personal"] : [],
|
|
310
|
+
setup: ["oauth"],
|
|
311
|
+
}),
|
|
312
|
+
),
|
|
313
|
+
ConnectProvider.parse({
|
|
314
|
+
id: "fiken-token",
|
|
315
|
+
label: "Fiken",
|
|
316
|
+
family: "fiken",
|
|
317
|
+
readiness:
|
|
318
|
+
!external || !canWrite
|
|
319
|
+
? "unsupported"
|
|
320
|
+
: credentialConfigured
|
|
321
|
+
? "available"
|
|
322
|
+
: "needs_configuration",
|
|
323
|
+
ownership: ["workspace"],
|
|
324
|
+
setup: ["credentials"],
|
|
325
|
+
}),
|
|
326
|
+
ConnectProvider.parse({
|
|
327
|
+
id: "fiken-oauth",
|
|
328
|
+
label: "Fiken (OAuth)",
|
|
329
|
+
family: "fiken",
|
|
330
|
+
readiness:
|
|
331
|
+
!external || !canWrite
|
|
332
|
+
? "unsupported"
|
|
333
|
+
: mcpConfigured &&
|
|
334
|
+
deps.settings.fikenClientId?.trim() &&
|
|
335
|
+
deps.settings.fikenClientSecret?.trim()
|
|
336
|
+
? "available"
|
|
337
|
+
: "needs_configuration",
|
|
338
|
+
ownership: ["workspace"],
|
|
339
|
+
setup: ["oauth"],
|
|
340
|
+
}),
|
|
341
|
+
ConnectProvider.parse({
|
|
342
|
+
id: "mcp-oauth",
|
|
343
|
+
label: "MCP server (OAuth)",
|
|
344
|
+
family: "mcp",
|
|
345
|
+
readiness:
|
|
346
|
+
!external || !canWrite
|
|
347
|
+
? "unsupported"
|
|
348
|
+
: mcpConfigured
|
|
349
|
+
? "available"
|
|
350
|
+
: "needs_configuration",
|
|
351
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
352
|
+
setup: ["credentials", "oauth"],
|
|
353
|
+
}),
|
|
354
|
+
ConnectProvider.parse({
|
|
355
|
+
id: "mcp-headers",
|
|
356
|
+
label: "MCP server (custom headers)",
|
|
357
|
+
family: "mcp",
|
|
358
|
+
readiness:
|
|
359
|
+
!external || !canWrite
|
|
360
|
+
? "unsupported"
|
|
361
|
+
: credentialConfigured
|
|
362
|
+
? "available"
|
|
363
|
+
: "needs_configuration",
|
|
364
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
365
|
+
setup: ["credentials"],
|
|
366
|
+
}),
|
|
367
|
+
ConnectProvider.parse({
|
|
368
|
+
id: "mcp-bearer",
|
|
369
|
+
label: "MCP server (bearer credential)",
|
|
370
|
+
family: "mcp",
|
|
371
|
+
readiness:
|
|
372
|
+
!external || !canWrite
|
|
373
|
+
? "unsupported"
|
|
374
|
+
: credentialConfigured
|
|
375
|
+
? "available"
|
|
376
|
+
: "needs_configuration",
|
|
377
|
+
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
378
|
+
setup: ["credentials"],
|
|
379
|
+
}),
|
|
380
|
+
]);
|
|
381
|
+
});
|
|
382
|
+
app.get("/v1/workspaces/:workspaceId/connect/accounts", async (c) => {
|
|
383
|
+
const workspaceId = c.req.param("workspaceId");
|
|
384
|
+
const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
|
|
385
|
+
const connections = hasPermission(grant.permissions, "connections:read")
|
|
386
|
+
? await listConnectionsMetadata(deps.db, workspaceId, grant.subjectId)
|
|
387
|
+
: [];
|
|
388
|
+
const githubInstallations =
|
|
389
|
+
hasPermission(grant.permissions, "github:use") ||
|
|
390
|
+
hasPermission(grant.permissions, "github:manage")
|
|
391
|
+
? await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId)
|
|
392
|
+
: [];
|
|
393
|
+
const lensRegistrations = await listPrReviewAppRegistrations(
|
|
394
|
+
deps.db,
|
|
395
|
+
grant.accountId,
|
|
396
|
+
workspaceId,
|
|
397
|
+
);
|
|
398
|
+
const socialConnections = await listSocialConnections(
|
|
399
|
+
deps.db,
|
|
400
|
+
workspaceId,
|
|
401
|
+
200,
|
|
402
|
+
grant.subjectId,
|
|
403
|
+
);
|
|
404
|
+
return c.json([
|
|
405
|
+
...socialConnections
|
|
406
|
+
.filter((connection) => ["x", "reddit"].includes(connection.provider))
|
|
407
|
+
.map((connection) =>
|
|
408
|
+
ConnectAccount.parse({
|
|
409
|
+
id: `social:${connection.id}`,
|
|
410
|
+
version: connection.version,
|
|
411
|
+
providerId: connection.provider,
|
|
412
|
+
label: connection.accountHandle,
|
|
413
|
+
ownership: connection.ownership,
|
|
414
|
+
status:
|
|
415
|
+
connection.status === "connected"
|
|
416
|
+
? "connected"
|
|
417
|
+
: connection.status === "disabled"
|
|
418
|
+
? "disabled"
|
|
419
|
+
: "auth_needed",
|
|
420
|
+
}),
|
|
421
|
+
),
|
|
422
|
+
...lensRegistrations
|
|
423
|
+
.filter((registration) => registration.credentialKind === "managed_github_app")
|
|
424
|
+
.map((registration) =>
|
|
425
|
+
ConnectAccount.parse({
|
|
426
|
+
id: `lens-registration:${registration.id}`,
|
|
427
|
+
providerId: "github-lens",
|
|
428
|
+
label: registration.providerAccountLogin ?? "Lens installation",
|
|
429
|
+
ownership: "workspace",
|
|
430
|
+
status: registration.status === "active" ? "connected" : "disabled",
|
|
431
|
+
}),
|
|
432
|
+
),
|
|
433
|
+
...githubInstallations.map((installation) =>
|
|
434
|
+
ConnectAccount.parse({
|
|
435
|
+
id: `github-installation:${installation.installationId}`,
|
|
436
|
+
providerId: "github-app",
|
|
437
|
+
label: installation.accountLogin ?? `Installation ${installation.installationId}`,
|
|
438
|
+
ownership: "workspace",
|
|
439
|
+
status: "connected",
|
|
440
|
+
}),
|
|
441
|
+
),
|
|
442
|
+
...connections.flatMap((connection) => {
|
|
443
|
+
if (isPersonalGitHubConnection(connection))
|
|
444
|
+
return [
|
|
445
|
+
ConnectAccount.parse({
|
|
446
|
+
id: connection.id,
|
|
447
|
+
version: connection.version,
|
|
448
|
+
providerId: "github-personal",
|
|
449
|
+
label: String(connection.metadata.githubLogin ?? "GitHub"),
|
|
450
|
+
ownership: "personal",
|
|
451
|
+
status:
|
|
452
|
+
connection.status === "revoked"
|
|
453
|
+
? "disabled"
|
|
454
|
+
: connection.status === "active"
|
|
455
|
+
? "connected"
|
|
456
|
+
: "auth_needed",
|
|
457
|
+
}),
|
|
458
|
+
];
|
|
459
|
+
if (isOpenGeniSlackBotConnection(connection))
|
|
460
|
+
return [
|
|
461
|
+
ConnectAccount.parse({
|
|
462
|
+
id: connection.id,
|
|
463
|
+
version: connection.version,
|
|
464
|
+
providerId: "slack-bot",
|
|
465
|
+
label: String(connection.metadata.slackTeamName ?? "Slack workspace bot"),
|
|
466
|
+
ownership: "workspace",
|
|
467
|
+
status:
|
|
468
|
+
connection.status === "revoked"
|
|
469
|
+
? "disabled"
|
|
470
|
+
: connection.status === "active"
|
|
471
|
+
? "connected"
|
|
472
|
+
: "auth_needed",
|
|
473
|
+
}),
|
|
474
|
+
];
|
|
475
|
+
if (connection.metadata.credentialRole === GOOGLE_DRIVE_CREDENTIAL_ROLE)
|
|
476
|
+
return [
|
|
477
|
+
ConnectAccount.parse({
|
|
478
|
+
id: connection.id,
|
|
479
|
+
version: connection.version,
|
|
480
|
+
providerId: googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
|
|
481
|
+
? "google-drive-publish"
|
|
482
|
+
: "google-drive-knowledge",
|
|
483
|
+
label: String(
|
|
484
|
+
connection.metadata.googleEmail ??
|
|
485
|
+
connection.metadata.googleDisplayName ??
|
|
486
|
+
"Google Drive",
|
|
487
|
+
),
|
|
488
|
+
ownership: "personal",
|
|
489
|
+
status:
|
|
490
|
+
connection.status === "revoked"
|
|
491
|
+
? "disabled"
|
|
492
|
+
: connection.status === "active"
|
|
493
|
+
? "connected"
|
|
494
|
+
: "auth_needed",
|
|
495
|
+
}),
|
|
496
|
+
];
|
|
497
|
+
if (connection.metadata.credentialRole === ATLASSIAN_CREDENTIAL_ROLE)
|
|
498
|
+
return [
|
|
499
|
+
ConnectAccount.parse({
|
|
500
|
+
id: connection.id,
|
|
501
|
+
version: connection.version,
|
|
502
|
+
providerId: "atlassian",
|
|
503
|
+
label: String(connection.metadata.displayName ?? "Atlassian"),
|
|
504
|
+
ownership: "personal",
|
|
505
|
+
status:
|
|
506
|
+
connection.status === "revoked"
|
|
507
|
+
? "disabled"
|
|
508
|
+
: connection.status === "active"
|
|
509
|
+
? "connected"
|
|
510
|
+
: "auth_needed",
|
|
511
|
+
}),
|
|
512
|
+
];
|
|
513
|
+
if (isFikenConnection(connection))
|
|
514
|
+
return [
|
|
515
|
+
ConnectAccount.parse({
|
|
516
|
+
id: connection.id,
|
|
517
|
+
version: connection.version,
|
|
518
|
+
providerId: connection.kind === "oauth2" ? "fiken-oauth" : "fiken-token",
|
|
519
|
+
label: "Fiken",
|
|
520
|
+
ownership: "workspace",
|
|
521
|
+
status:
|
|
522
|
+
connection.status === "revoked"
|
|
523
|
+
? "disabled"
|
|
524
|
+
: connection.status === "active"
|
|
525
|
+
? "connected"
|
|
526
|
+
: "auth_needed",
|
|
527
|
+
}),
|
|
528
|
+
];
|
|
529
|
+
if (
|
|
530
|
+
connection.kind === "api_key" &&
|
|
531
|
+
["mcp-bearer", "mcp-headers"].includes(String(connection.metadata.connectAdapter))
|
|
532
|
+
)
|
|
533
|
+
return [
|
|
534
|
+
ConnectAccount.parse({
|
|
535
|
+
id: connection.id,
|
|
536
|
+
version: connection.version,
|
|
537
|
+
providerId: connection.metadata.connectAdapter,
|
|
538
|
+
label: connection.providerDomain,
|
|
539
|
+
ownership: connection.subjectId === null ? "workspace" : "personal",
|
|
540
|
+
status:
|
|
541
|
+
connection.status === "revoked"
|
|
542
|
+
? "disabled"
|
|
543
|
+
: connection.status === "active"
|
|
544
|
+
? "connected"
|
|
545
|
+
: "auth_needed",
|
|
546
|
+
}),
|
|
547
|
+
];
|
|
548
|
+
if (
|
|
549
|
+
connection.kind === "oauth2" &&
|
|
550
|
+
connection.metadata.oauthDiscovery &&
|
|
551
|
+
typeof connection.metadata.mcpUrl === "string"
|
|
552
|
+
)
|
|
553
|
+
return [
|
|
554
|
+
ConnectAccount.parse({
|
|
555
|
+
id: connection.id,
|
|
556
|
+
version: connection.version,
|
|
557
|
+
providerId:
|
|
558
|
+
connection.metadata.mcpUrl === OPENGENI_PERSONAL_SLACK_MCP_URL
|
|
559
|
+
? "slack-personal"
|
|
560
|
+
: "mcp-oauth",
|
|
561
|
+
label: connection.providerDomain,
|
|
562
|
+
ownership: connection.subjectId === null ? "workspace" : "personal",
|
|
563
|
+
status:
|
|
564
|
+
connection.status === "revoked"
|
|
565
|
+
? "disabled"
|
|
566
|
+
: connection.status === "active"
|
|
567
|
+
? "connected"
|
|
568
|
+
: "auth_needed",
|
|
569
|
+
}),
|
|
570
|
+
];
|
|
571
|
+
if (connection.metadata.credentialRole !== API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) return [];
|
|
572
|
+
const ids = connection.metadata.authorizedDefinitionIds;
|
|
573
|
+
if (!Array.isArray(ids)) return [];
|
|
574
|
+
return CORE_INTEGRATION_DEFINITIONS.filter(
|
|
575
|
+
(definition) =>
|
|
576
|
+
ids.includes(definition.id) &&
|
|
577
|
+
definition.provider.id === connection.metadata.providerFamily,
|
|
578
|
+
).map((definition) =>
|
|
579
|
+
ConnectAccount.parse({
|
|
580
|
+
id: connection.id,
|
|
581
|
+
version: connection.version,
|
|
582
|
+
providerId: definition.id,
|
|
583
|
+
label: String(
|
|
584
|
+
connection.metadata.providerDisplayName ??
|
|
585
|
+
connection.metadata.providerEmail ??
|
|
586
|
+
connection.metadata.providerPrincipalId ??
|
|
587
|
+
connection.id,
|
|
588
|
+
).slice(0, 256),
|
|
589
|
+
ownership: connection.subjectId === null ? "workspace" : "personal",
|
|
590
|
+
status:
|
|
591
|
+
connection.status === "revoked"
|
|
592
|
+
? "disabled"
|
|
593
|
+
: connection.status !== "active"
|
|
594
|
+
? "auth_needed"
|
|
595
|
+
: "connected",
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
598
|
+
}),
|
|
599
|
+
]);
|
|
600
|
+
});
|
|
601
|
+
const transport = createPinnedIntegrationTransport({
|
|
602
|
+
network: deps.settings,
|
|
603
|
+
...(deps.apiIntegrationSourceFetch ? { fetchImpl: deps.apiIntegrationSourceFetch } : {}),
|
|
604
|
+
});
|
|
605
|
+
app.post("/v1/workspaces/:workspaceId/connect/attempts/:attemptId/advance", async (c) => {
|
|
606
|
+
const input = AdvanceConnectRequest.parse(await c.req.json());
|
|
607
|
+
const action = input.action;
|
|
608
|
+
const workspaceId = c.req.param("workspaceId");
|
|
609
|
+
const permission =
|
|
610
|
+
action.type === "install"
|
|
611
|
+
? "capabilities:manage"
|
|
612
|
+
: action.type === "credentials"
|
|
613
|
+
? "connections:write"
|
|
614
|
+
: "workspace:read";
|
|
615
|
+
// Resolve the authenticated workspace actor first, then enforce the exact
|
|
616
|
+
// stored provider/action permission below. Setup managers need not also
|
|
617
|
+
// carry the unrelated workspace metadata read permission.
|
|
618
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
619
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
620
|
+
if (!authorization.contextIntegrity)
|
|
621
|
+
throw new HTTPException(403, { message: "Verified Connect authority required" });
|
|
622
|
+
const scope = {
|
|
623
|
+
accountId: authorization.grant.accountId,
|
|
624
|
+
workspaceId,
|
|
625
|
+
subjectId: authorization.grant.subjectId,
|
|
626
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
|
|
627
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
628
|
+
};
|
|
629
|
+
// Reject invalid caller selections before taking a durable operation claim.
|
|
630
|
+
// Older revisions still go through the receipt-aware claim path for replay.
|
|
631
|
+
const stored = await getConnectAttempt(deps.db, scope, c.req.param("attemptId"));
|
|
632
|
+
const before = stored.attempt;
|
|
633
|
+
const advancePermission =
|
|
634
|
+
before.providerId === "mcp-install" ? "capabilities:manage" : permission;
|
|
635
|
+
if (!hasPermission(authorization.grant.permissions, advancePermission))
|
|
636
|
+
throw new HTTPException(403, { message: `${advancePermission} required` });
|
|
637
|
+
if (before.providerId === "mcp-install") {
|
|
638
|
+
if (action.type !== "credentials")
|
|
639
|
+
throw new HTTPException(422, { message: "Choose an MCP capability" });
|
|
640
|
+
const values = z
|
|
641
|
+
.object({ capabilityId: z.string().min(1).max(512) })
|
|
642
|
+
.strict()
|
|
643
|
+
.parse(action.values);
|
|
644
|
+
return c.json(
|
|
645
|
+
await executeConnectOperation({
|
|
646
|
+
db: deps.db,
|
|
647
|
+
scope,
|
|
648
|
+
attemptId: before.id,
|
|
649
|
+
expectedRevision: input.expectedRevision,
|
|
650
|
+
operationId: input.idempotencyKey,
|
|
651
|
+
inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
652
|
+
authorize: (tx, _attempt, origin) =>
|
|
653
|
+
requireConnectOwnerAuthority(tx, scope, "capabilities:manage", origin),
|
|
654
|
+
execute: async (attempt) => {
|
|
655
|
+
if (attempt.state !== "credential_input")
|
|
656
|
+
throw new HTTPException(409, { message: "Reload MCP setup" });
|
|
657
|
+
const prepared = await prepareCapabilityEnable({
|
|
658
|
+
db: deps.db,
|
|
659
|
+
settings: deps.settings,
|
|
660
|
+
grant: authorization.grant,
|
|
661
|
+
accountId: scope.accountId,
|
|
662
|
+
workspaceId,
|
|
663
|
+
capabilityId: values.capabilityId,
|
|
664
|
+
payload: { config: {}, metadata: {}, headers: {} },
|
|
665
|
+
});
|
|
666
|
+
return {
|
|
667
|
+
commit: async (tx, current) => {
|
|
668
|
+
await prepared.commit(tx);
|
|
669
|
+
return {
|
|
670
|
+
...current,
|
|
671
|
+
revision: current.revision + 1,
|
|
672
|
+
state: "complete",
|
|
673
|
+
completionRequirement: "provider_setup",
|
|
674
|
+
integrationInstalled: true,
|
|
675
|
+
nextAction: { type: "none" },
|
|
676
|
+
};
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (["github-app", "github-lens"].includes(before.providerId)) {
|
|
684
|
+
const installationPermission =
|
|
685
|
+
before.providerId === "github-lens" ? "workspace:admin" : "github:manage";
|
|
686
|
+
if (
|
|
687
|
+
!hasPermission(authorization.grant.permissions, installationPermission) ||
|
|
688
|
+
(before.providerId === "github-lens" &&
|
|
689
|
+
!hasPermission(authorization.grant.permissions, "secrets:write"))
|
|
690
|
+
)
|
|
691
|
+
throw new HTTPException(403, { message: "Installation management permission required" });
|
|
692
|
+
return c.json(
|
|
693
|
+
await executeConnectOperation({
|
|
694
|
+
db: deps.db,
|
|
695
|
+
scope,
|
|
696
|
+
attemptId: before.id,
|
|
697
|
+
expectedRevision: input.expectedRevision,
|
|
698
|
+
operationId: input.idempotencyKey,
|
|
699
|
+
inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
700
|
+
authorize: async (tx, _attempt, origin) => {
|
|
701
|
+
await requireConnectOwnerAuthority(tx, scope, installationPermission, origin);
|
|
702
|
+
if (before.providerId === "github-lens") {
|
|
703
|
+
await requireConnectOwnerAuthority(tx, scope, "secrets:write", origin);
|
|
704
|
+
await requireGitHubLensConnect({ ...deps, db: tx }, workspaceId);
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
execute: async (attempt) =>
|
|
708
|
+
prepareGitHubAppConnectAction(deps, scope, c.req.url, attempt, action),
|
|
709
|
+
}),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (["openapi", "graphql"].includes(before.providerId) && action.type === "credentials") {
|
|
713
|
+
const values = z
|
|
714
|
+
.object({
|
|
715
|
+
url: z.string().url().max(2048),
|
|
716
|
+
connectionId: z.string().uuid().or(z.literal("")).optional(),
|
|
717
|
+
})
|
|
718
|
+
.strict()
|
|
719
|
+
.parse(action.values);
|
|
720
|
+
const source =
|
|
721
|
+
before.providerId === "openapi"
|
|
722
|
+
? { kind: "openapi" as const, url: values.url }
|
|
723
|
+
: { kind: "graphql" as const, endpoint: values.url };
|
|
724
|
+
const connectionId = values.connectionId || undefined;
|
|
725
|
+
return c.json(
|
|
726
|
+
await executeConnectOperation({
|
|
727
|
+
db: deps.db,
|
|
728
|
+
scope,
|
|
729
|
+
attemptId: before.id,
|
|
730
|
+
expectedRevision: input.expectedRevision,
|
|
731
|
+
operationId: input.idempotencyKey,
|
|
732
|
+
inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
733
|
+
authorize: (tx, _attempt, origin) =>
|
|
734
|
+
requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
|
|
735
|
+
execute: async (attempt) => {
|
|
736
|
+
if (attempt.state !== "credential_input")
|
|
737
|
+
throw new HTTPException(409, { message: "Reload setup before changing the source" });
|
|
738
|
+
const resolved = await resolveForRoute({
|
|
739
|
+
deps,
|
|
740
|
+
transport,
|
|
741
|
+
workspaceId,
|
|
742
|
+
subjectId: scope.subjectId,
|
|
743
|
+
payload: {
|
|
744
|
+
source,
|
|
745
|
+
ownership: attempt.ownership,
|
|
746
|
+
...(connectionId ? { connectionId } : {}),
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
const account = connectionId
|
|
750
|
+
? await getConnectionMetadata(deps.db, workspaceId, connectionId, scope.subjectId)
|
|
751
|
+
: null;
|
|
752
|
+
if (
|
|
753
|
+
connectionId &&
|
|
754
|
+
(!account ||
|
|
755
|
+
(account.subjectId === null ? "workspace" : "personal") !== attempt.ownership)
|
|
756
|
+
)
|
|
757
|
+
throw new HTTPException(404, {
|
|
758
|
+
message: "Connection unavailable for the requested ownership",
|
|
759
|
+
});
|
|
760
|
+
if (!account && attempt.ownership === "personal")
|
|
761
|
+
throw new HTTPException(422, {
|
|
762
|
+
message: "Personal installation requires a personal connection",
|
|
763
|
+
});
|
|
764
|
+
if (!account && resolved.preview.auth.kind !== "none")
|
|
765
|
+
throw new HTTPException(422, {
|
|
766
|
+
message: "Select a connection for this authenticated service",
|
|
767
|
+
});
|
|
768
|
+
return {
|
|
769
|
+
commit: async (_tx, current) => ({
|
|
770
|
+
...current,
|
|
771
|
+
source,
|
|
772
|
+
revision: current.revision + 1,
|
|
773
|
+
state: "preview",
|
|
774
|
+
credentialsCommitted: !!account,
|
|
775
|
+
...(account
|
|
776
|
+
? {
|
|
777
|
+
account: {
|
|
778
|
+
id: account.id,
|
|
779
|
+
version: account.version,
|
|
780
|
+
providerId: attempt.providerId,
|
|
781
|
+
label: account.providerDomain,
|
|
782
|
+
ownership: attempt.ownership,
|
|
783
|
+
status: "connected" as const,
|
|
784
|
+
},
|
|
785
|
+
}
|
|
786
|
+
: {}),
|
|
787
|
+
nextAction: {
|
|
788
|
+
type: "preview",
|
|
789
|
+
previewId: resolved.preview.revisionId,
|
|
790
|
+
contentHash: resolved.preview.contentSha256,
|
|
791
|
+
operations: resolved.preview.tools.map((tool) => ({
|
|
792
|
+
id: tool.id,
|
|
793
|
+
label: tool.name,
|
|
794
|
+
kind: tool.safety,
|
|
795
|
+
})),
|
|
796
|
+
},
|
|
797
|
+
}),
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
}),
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (before.providerId === "fiken-token") {
|
|
804
|
+
if (action.type !== "credentials" || before.ownership !== "workspace")
|
|
805
|
+
throw new HTTPException(422, { message: "Fiken requires workspace-owned token setup" });
|
|
806
|
+
const values = z
|
|
807
|
+
.object({
|
|
808
|
+
apiToken: z.string().min(1).max(16_384),
|
|
809
|
+
defaultCompanySlug: z.string().max(128).optional(),
|
|
810
|
+
})
|
|
811
|
+
.strict()
|
|
812
|
+
.parse(action.values);
|
|
813
|
+
return c.json(
|
|
814
|
+
await executeConnectOperation({
|
|
815
|
+
db: deps.db,
|
|
816
|
+
scope,
|
|
817
|
+
attemptId: before.id,
|
|
818
|
+
expectedRevision: input.expectedRevision,
|
|
819
|
+
operationId: input.idempotencyKey,
|
|
820
|
+
inputDigest: createHmac("sha256", requireEnvironmentEncryption(deps.settings))
|
|
821
|
+
.update(stableJson(input))
|
|
822
|
+
.digest("hex"),
|
|
823
|
+
authorize: (tx, _attempt, origin) =>
|
|
824
|
+
requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
|
|
825
|
+
execute: async (attempt) => {
|
|
826
|
+
if (attempt.state !== "credential_input")
|
|
827
|
+
throw new HTTPException(409, { message: "Reload Fiken setup" });
|
|
828
|
+
const persist = await prepareFikenTokenInstall(
|
|
829
|
+
deps,
|
|
830
|
+
scope,
|
|
831
|
+
{
|
|
832
|
+
apiToken: values.apiToken,
|
|
833
|
+
...(values.defaultCompanySlug
|
|
834
|
+
? { defaultCompanySlug: values.defaultCompanySlug }
|
|
835
|
+
: {}),
|
|
836
|
+
...(attempt.account ? { connectionId: attempt.account.id } : {}),
|
|
837
|
+
},
|
|
838
|
+
attempt.account?.version,
|
|
839
|
+
);
|
|
840
|
+
return {
|
|
841
|
+
commit: async (tx, current) => {
|
|
842
|
+
const connection = await persist(tx);
|
|
843
|
+
return {
|
|
844
|
+
...current,
|
|
845
|
+
revision: current.revision + 1,
|
|
846
|
+
state: "complete",
|
|
847
|
+
credentialsCommitted: true,
|
|
848
|
+
nextAction: { type: "none" },
|
|
849
|
+
account: {
|
|
850
|
+
id: connection.id,
|
|
851
|
+
version: connection.version,
|
|
852
|
+
providerId: "fiken-token",
|
|
853
|
+
label: "Fiken",
|
|
854
|
+
ownership: "workspace",
|
|
855
|
+
status: "connected",
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
},
|
|
861
|
+
}),
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (["mcp-bearer", "mcp-headers"].includes(before.providerId)) {
|
|
865
|
+
if (action.type !== "credentials")
|
|
866
|
+
throw new HTTPException(422, { message: "MCP credentials required" });
|
|
867
|
+
const values =
|
|
868
|
+
before.providerId === "mcp-headers"
|
|
869
|
+
? z
|
|
870
|
+
.object({
|
|
871
|
+
mcpUrl: z.string().url().max(4096),
|
|
872
|
+
headers: z.string().min(2).max(65_536),
|
|
873
|
+
})
|
|
874
|
+
.strict()
|
|
875
|
+
.parse(action.values)
|
|
876
|
+
: z
|
|
877
|
+
.object({
|
|
878
|
+
mcpUrl: z.string().url().max(4096),
|
|
879
|
+
token: z
|
|
880
|
+
.string()
|
|
881
|
+
.min(1)
|
|
882
|
+
.max(16_384)
|
|
883
|
+
.regex(/^[^\u0000-\u0020\u007f]+$/),
|
|
884
|
+
})
|
|
885
|
+
.strict()
|
|
886
|
+
.parse(action.values);
|
|
887
|
+
let credentialHeaders: Record<string, string>;
|
|
888
|
+
try {
|
|
889
|
+
credentialHeaders =
|
|
890
|
+
"headers" in values
|
|
891
|
+
? normalizedHostCredentialHeaders(
|
|
892
|
+
z.record(z.string(), z.string()).parse(JSON.parse(values.headers)),
|
|
893
|
+
)
|
|
894
|
+
: { authorization: `Bearer ${values.token}` };
|
|
895
|
+
} catch {
|
|
896
|
+
throw new HTTPException(422, {
|
|
897
|
+
message: "Supply a JSON object of valid credential headers",
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const destination = new URL(values.mcpUrl);
|
|
901
|
+
if (
|
|
902
|
+
[WORKSPACE_OPENROUTER_CONNECTION_DOMAIN, VERCEL_AI_GATEWAY_CONNECTION_DOMAIN].some(
|
|
903
|
+
(domain) => domain === destination.hostname,
|
|
904
|
+
)
|
|
905
|
+
)
|
|
906
|
+
throw new HTTPException(422, {
|
|
907
|
+
message: "Use the dedicated workspace provider credential flow",
|
|
908
|
+
});
|
|
909
|
+
if (
|
|
910
|
+
destination.protocol !== "https:" ||
|
|
911
|
+
destination.username ||
|
|
912
|
+
destination.password ||
|
|
913
|
+
destination.hash
|
|
914
|
+
)
|
|
915
|
+
throw new HTTPException(422, {
|
|
916
|
+
message: "MCP credential destination requires HTTPS without userinfo or fragment",
|
|
917
|
+
});
|
|
918
|
+
const encryptionKey = requireEnvironmentEncryption(deps.settings);
|
|
919
|
+
const mcpUrl = destination.toString();
|
|
920
|
+
return c.json(
|
|
921
|
+
await executeConnectOperation({
|
|
922
|
+
db: deps.db,
|
|
923
|
+
scope,
|
|
924
|
+
attemptId: before.id,
|
|
925
|
+
expectedRevision: input.expectedRevision,
|
|
926
|
+
operationId: input.idempotencyKey,
|
|
927
|
+
inputDigest: createHmac("sha256", encryptionKey).update(stableJson(input)).digest("hex"),
|
|
928
|
+
authorize: (tx, _attempt, origin) =>
|
|
929
|
+
requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
|
|
930
|
+
execute: async (attempt) => {
|
|
931
|
+
if (attempt.state !== "credential_input")
|
|
932
|
+
throw new HTTPException(409, { message: "Reload the current credential setup" });
|
|
933
|
+
const credentialEncrypted = encryptEnvironmentValue(
|
|
934
|
+
encryptionKey,
|
|
935
|
+
JSON.stringify({ headers: credentialHeaders }),
|
|
936
|
+
);
|
|
937
|
+
return {
|
|
938
|
+
commit: async (tx, current) => {
|
|
939
|
+
const existing = current.account
|
|
940
|
+
? await getConnectionMetadata(
|
|
941
|
+
tx,
|
|
942
|
+
workspaceId,
|
|
943
|
+
current.account.id,
|
|
944
|
+
scope.subjectId,
|
|
945
|
+
)
|
|
946
|
+
: null;
|
|
947
|
+
if (
|
|
948
|
+
current.account &&
|
|
949
|
+
(!existing ||
|
|
950
|
+
existing.metadata.connectAdapter !== current.providerId ||
|
|
951
|
+
existing.metadata.mcpUrl !== mcpUrl ||
|
|
952
|
+
existing.version !== current.account.version ||
|
|
953
|
+
(existing.subjectId === null ? "workspace" : "personal") !== current.ownership)
|
|
954
|
+
)
|
|
955
|
+
throw new HTTPException(409, {
|
|
956
|
+
message: "MCP account changed; reload before replacing credentials",
|
|
957
|
+
});
|
|
958
|
+
const metadata = { connectAdapter: current.providerId, mcpUrl, resource: mcpUrl };
|
|
959
|
+
const subjectId = current.ownership === "personal" ? scope.subjectId : null;
|
|
960
|
+
const connection = existing
|
|
961
|
+
? await updateConnection(tx, {
|
|
962
|
+
workspaceId,
|
|
963
|
+
connectionId: existing.id,
|
|
964
|
+
expectedVersion: existing.version,
|
|
965
|
+
visibleToSubjectId: scope.subjectId,
|
|
966
|
+
subjectId,
|
|
967
|
+
providerDomain: destination.hostname,
|
|
968
|
+
kind: "api_key",
|
|
969
|
+
status: "active",
|
|
970
|
+
credentialEncrypted,
|
|
971
|
+
grantedScopes: [],
|
|
972
|
+
expiresAt: null,
|
|
973
|
+
metadata,
|
|
974
|
+
updatedBySubjectId: scope.subjectId,
|
|
975
|
+
})
|
|
976
|
+
: await createConnection(tx, {
|
|
977
|
+
accountId: scope.accountId,
|
|
978
|
+
workspaceId,
|
|
979
|
+
subjectId,
|
|
980
|
+
providerDomain: destination.hostname,
|
|
981
|
+
kind: "api_key",
|
|
982
|
+
credentialEncrypted,
|
|
983
|
+
grantedScopes: [],
|
|
984
|
+
expiresAt: null,
|
|
985
|
+
metadata,
|
|
986
|
+
createdBySubjectId: scope.subjectId,
|
|
987
|
+
});
|
|
988
|
+
if (!connection) throw new HTTPException(409, { message: "MCP account changed" });
|
|
989
|
+
return {
|
|
990
|
+
...current,
|
|
991
|
+
revision: current.revision + 1,
|
|
992
|
+
state: "complete",
|
|
993
|
+
credentialsCommitted: true,
|
|
994
|
+
nextAction: { type: "none" },
|
|
995
|
+
account: {
|
|
996
|
+
id: connection.id,
|
|
997
|
+
version: connection.version,
|
|
998
|
+
providerId: current.providerId,
|
|
999
|
+
label: connection.providerDomain,
|
|
1000
|
+
ownership: current.ownership,
|
|
1001
|
+
status: "connected",
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
},
|
|
1005
|
+
};
|
|
1006
|
+
},
|
|
1007
|
+
}),
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
if (["mcp-oauth", "slack-personal"].includes(before.providerId)) {
|
|
1011
|
+
if (action.type !== "credentials")
|
|
1012
|
+
throw new HTTPException(422, { message: "MCP OAuth requires a server URL" });
|
|
1013
|
+
const values =
|
|
1014
|
+
before.providerId === "slack-personal"
|
|
1015
|
+
? { mcpUrl: OPENGENI_PERSONAL_SLACK_MCP_URL }
|
|
1016
|
+
: z
|
|
1017
|
+
.object({ mcpUrl: z.string().url().max(4096) })
|
|
1018
|
+
.strict()
|
|
1019
|
+
.parse(action.values);
|
|
1020
|
+
return c.json(
|
|
1021
|
+
await executeConnectOperation({
|
|
1022
|
+
db: deps.db,
|
|
1023
|
+
scope,
|
|
1024
|
+
attemptId: before.id,
|
|
1025
|
+
expectedRevision: input.expectedRevision,
|
|
1026
|
+
operationId: input.idempotencyKey,
|
|
1027
|
+
inputDigest: createHmac("sha256", requireIntegrationsStateSecret(deps.settings))
|
|
1028
|
+
.update(stableJson(input))
|
|
1029
|
+
.digest("hex"),
|
|
1030
|
+
authorize: (tx, _attempt, origin) =>
|
|
1031
|
+
requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
|
|
1032
|
+
execute: async (attempt) => {
|
|
1033
|
+
if (attempt.state !== "credential_input")
|
|
1034
|
+
throw new HTTPException(409, { message: "Reload the current connection setup" });
|
|
1035
|
+
const started = await startMcpOAuth(deps, {
|
|
1036
|
+
...scope,
|
|
1037
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1038
|
+
connectAttemptId: attempt.id,
|
|
1039
|
+
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(authorization),
|
|
1040
|
+
requestUrl: c.req.url,
|
|
1041
|
+
payload: {
|
|
1042
|
+
requestedScopes: [],
|
|
1043
|
+
mcpUrl: values.mcpUrl,
|
|
1044
|
+
ownership: attempt.ownership,
|
|
1045
|
+
returnUrl: stored.returnUrl,
|
|
1046
|
+
...(attempt.account ? { connectionId: attempt.account.id } : {}),
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
const authorizationUrl = started.authorizationUrl;
|
|
1050
|
+
if (!authorizationUrl)
|
|
1051
|
+
throw new HTTPException(502, {
|
|
1052
|
+
message: "provider did not return an authorization URL",
|
|
1053
|
+
});
|
|
1054
|
+
return {
|
|
1055
|
+
commit: async (_tx, current) => ({
|
|
1056
|
+
...current,
|
|
1057
|
+
revision: current.revision + 1,
|
|
1058
|
+
state: "requires_user_action",
|
|
1059
|
+
nextAction: { type: "authorize", url: authorizationUrl },
|
|
1060
|
+
}),
|
|
1061
|
+
};
|
|
1062
|
+
},
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (action.type !== "retry" && action.type !== "install")
|
|
1067
|
+
throw new HTTPException(422, {
|
|
1068
|
+
message: "this provider action is not supported by curated OAuth",
|
|
1069
|
+
});
|
|
1070
|
+
if (before.revision === input.expectedRevision) {
|
|
1071
|
+
if (
|
|
1072
|
+
(!before.source && (!before.credentialsCommitted || !before.account)) ||
|
|
1073
|
+
!["connected_but_incomplete", "preview"].includes(before.state)
|
|
1074
|
+
)
|
|
1075
|
+
throw new HTTPException(409, { message: "connection is not ready for preview or install" });
|
|
1076
|
+
if (action.type === "install") {
|
|
1077
|
+
const preview = before.nextAction;
|
|
1078
|
+
if (
|
|
1079
|
+
preview.type !== "preview" ||
|
|
1080
|
+
preview.previewId !== action.previewId ||
|
|
1081
|
+
preview.contentHash !== action.contentHash ||
|
|
1082
|
+
action.operationIds.some(
|
|
1083
|
+
(id) => !preview.operations.some((operation) => operation.id === id),
|
|
1084
|
+
)
|
|
1085
|
+
)
|
|
1086
|
+
throw new HTTPException(409, {
|
|
1087
|
+
message: "review the current preview before installation",
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return c.json(
|
|
1092
|
+
await executeConnectOperation({
|
|
1093
|
+
db: deps.db,
|
|
1094
|
+
scope,
|
|
1095
|
+
attemptId: c.req.param("attemptId"),
|
|
1096
|
+
expectedRevision: input.expectedRevision,
|
|
1097
|
+
operationId: input.idempotencyKey,
|
|
1098
|
+
inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1099
|
+
authorize: (tx, _attempt, origin) =>
|
|
1100
|
+
requireConnectOwnerAuthority(tx, scope, permission, origin),
|
|
1101
|
+
execute: async (attempt) => {
|
|
1102
|
+
if (
|
|
1103
|
+
(!attempt.source && (!attempt.credentialsCommitted || !attempt.account)) ||
|
|
1104
|
+
!["connected_but_incomplete", "preview"].includes(attempt.state)
|
|
1105
|
+
)
|
|
1106
|
+
throw new HTTPException(409, {
|
|
1107
|
+
message: "connection is not ready for preview or install",
|
|
1108
|
+
});
|
|
1109
|
+
if (
|
|
1110
|
+
action.type === "install" &&
|
|
1111
|
+
(attempt.nextAction.type !== "preview" ||
|
|
1112
|
+
attempt.nextAction.previewId !== action.previewId ||
|
|
1113
|
+
attempt.nextAction.contentHash !== action.contentHash)
|
|
1114
|
+
)
|
|
1115
|
+
throw new HTTPException(409, {
|
|
1116
|
+
message: "review the current preview before installation",
|
|
1117
|
+
});
|
|
1118
|
+
const previewRequest = {
|
|
1119
|
+
source: attempt.source ?? {
|
|
1120
|
+
kind: "definition" as const,
|
|
1121
|
+
definitionId: attempt.providerId,
|
|
1122
|
+
},
|
|
1123
|
+
...(attempt.account ? { connectionId: attempt.account.id } : {}),
|
|
1124
|
+
ownership: attempt.ownership,
|
|
1125
|
+
};
|
|
1126
|
+
const resolved = await resolveForRoute({
|
|
1127
|
+
deps,
|
|
1128
|
+
transport,
|
|
1129
|
+
workspaceId,
|
|
1130
|
+
subjectId: scope.subjectId,
|
|
1131
|
+
payload: previewRequest,
|
|
1132
|
+
});
|
|
1133
|
+
const preparedPreview = (changed = false) => ({
|
|
1134
|
+
commit: async () => ({
|
|
1135
|
+
...attempt,
|
|
1136
|
+
error: changed
|
|
1137
|
+
? {
|
|
1138
|
+
code: "source_changed",
|
|
1139
|
+
message: "The source changed; review the new operations before installing.",
|
|
1140
|
+
retryable: true,
|
|
1141
|
+
}
|
|
1142
|
+
: undefined,
|
|
1143
|
+
revision: attempt.revision + 1,
|
|
1144
|
+
state: "preview" as const,
|
|
1145
|
+
nextAction: {
|
|
1146
|
+
type: "preview" as const,
|
|
1147
|
+
previewId: resolved.preview.revisionId,
|
|
1148
|
+
contentHash: resolved.preview.contentSha256,
|
|
1149
|
+
operations: resolved.preview.tools.map((tool) => ({
|
|
1150
|
+
id: tool.id,
|
|
1151
|
+
label: tool.name,
|
|
1152
|
+
kind: tool.safety,
|
|
1153
|
+
})),
|
|
1154
|
+
},
|
|
1155
|
+
}),
|
|
1156
|
+
});
|
|
1157
|
+
if (action.type === "retry") return preparedPreview();
|
|
1158
|
+
// Always pass the explicit selection; omission must never select all tools.
|
|
1159
|
+
const payload = InstallApiIntegrationRequest.parse({
|
|
1160
|
+
...previewRequest,
|
|
1161
|
+
expectedRevisionId: action.previewId,
|
|
1162
|
+
expectedContentSha256: action.contentHash,
|
|
1163
|
+
allowedTools: action.operationIds,
|
|
1164
|
+
...attempt.installationTarget,
|
|
1165
|
+
});
|
|
1166
|
+
let installation: ReturnType<typeof validatedIntegrationInstallInput>;
|
|
1167
|
+
try {
|
|
1168
|
+
installation = validatedIntegrationInstallInput(
|
|
1169
|
+
authorization.grant,
|
|
1170
|
+
workspaceId,
|
|
1171
|
+
payload,
|
|
1172
|
+
resolved,
|
|
1173
|
+
);
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
// The shared validator's 409 means the reviewed source changed.
|
|
1176
|
+
// No installation write has run; persist a fresh, unselected preview.
|
|
1177
|
+
if (error instanceof HTTPException && error.status === 409)
|
|
1178
|
+
return preparedPreview(true);
|
|
1179
|
+
throw error;
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
commit: async (tx) => {
|
|
1183
|
+
await installApiIntegration(tx, installation);
|
|
1184
|
+
return {
|
|
1185
|
+
...attempt,
|
|
1186
|
+
error: undefined,
|
|
1187
|
+
revision: attempt.revision + 1,
|
|
1188
|
+
state: "complete" as const,
|
|
1189
|
+
integrationInstalled: true,
|
|
1190
|
+
nextAction: { type: "none" as const },
|
|
1191
|
+
};
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
},
|
|
1195
|
+
}),
|
|
1196
|
+
);
|
|
1197
|
+
});
|
|
1198
|
+
app.post("/v1/workspaces/:workspaceId/connect/attempts/:attemptId/cancel", async (c) => {
|
|
1199
|
+
const input = ConnectOperationRequest.parse(await c.req.json());
|
|
1200
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1201
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1202
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
1203
|
+
if (!authorization.contextIntegrity)
|
|
1204
|
+
throw new HTTPException(403, { message: "Verified Connect authority required" });
|
|
1205
|
+
const scope = {
|
|
1206
|
+
accountId: authorization.grant.accountId,
|
|
1207
|
+
workspaceId,
|
|
1208
|
+
subjectId: authorization.grant.subjectId,
|
|
1209
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
|
|
1210
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1211
|
+
};
|
|
1212
|
+
return c.json(
|
|
1213
|
+
await executeConnectOperation({
|
|
1214
|
+
db: deps.db,
|
|
1215
|
+
scope,
|
|
1216
|
+
attemptId: c.req.param("attemptId"),
|
|
1217
|
+
expectedRevision: input.expectedRevision,
|
|
1218
|
+
operationId: input.idempotencyKey,
|
|
1219
|
+
inputDigest: createHash("sha256")
|
|
1220
|
+
.update(stableJson({ ...input, action: "cancel" }))
|
|
1221
|
+
.digest("hex"),
|
|
1222
|
+
authorize: async (tx, attempt, origin) => {
|
|
1223
|
+
const permission =
|
|
1224
|
+
attempt.providerId === "mcp-install"
|
|
1225
|
+
? "capabilities:manage"
|
|
1226
|
+
: ["x", "reddit"].includes(attempt.providerId)
|
|
1227
|
+
? attempt.ownership === "workspace"
|
|
1228
|
+
? "workspace:admin"
|
|
1229
|
+
: "workspace:read"
|
|
1230
|
+
: attempt.providerId === "github-app"
|
|
1231
|
+
? "github:manage"
|
|
1232
|
+
: attempt.providerId === "github-lens"
|
|
1233
|
+
? "workspace:admin"
|
|
1234
|
+
: "connections:write";
|
|
1235
|
+
if (
|
|
1236
|
+
!hasPermission(authorization.grant.permissions, permission) ||
|
|
1237
|
+
(attempt.providerId === "github-lens" &&
|
|
1238
|
+
!hasPermission(authorization.grant.permissions, "secrets:write"))
|
|
1239
|
+
)
|
|
1240
|
+
throw new HTTPException(403, { message: "Connect management permission required" });
|
|
1241
|
+
await requireConnectOwnerAuthority(tx, scope, permission, origin);
|
|
1242
|
+
if (attempt.providerId === "github-lens")
|
|
1243
|
+
await requireConnectOwnerAuthority(tx, scope, "secrets:write", origin);
|
|
1244
|
+
},
|
|
1245
|
+
execute: async () => ({
|
|
1246
|
+
commit: async (_tx, current) => ({
|
|
1247
|
+
...current,
|
|
1248
|
+
revision: current.revision + 1,
|
|
1249
|
+
state: "cancelled",
|
|
1250
|
+
nextAction: { type: "none" },
|
|
1251
|
+
}),
|
|
1252
|
+
}),
|
|
1253
|
+
}),
|
|
1254
|
+
);
|
|
1255
|
+
});
|
|
1256
|
+
app.post("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
|
|
1257
|
+
if (!deps.settings.integrationsEnabled)
|
|
1258
|
+
throw new HTTPException(403, { message: "integrations disabled" });
|
|
1259
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1260
|
+
const input = BeginConnectRequest.parse(await c.req.json());
|
|
1261
|
+
const setupPermission =
|
|
1262
|
+
input.providerId === "mcp-install"
|
|
1263
|
+
? "capabilities:manage"
|
|
1264
|
+
: ["x", "reddit"].includes(input.providerId)
|
|
1265
|
+
? input.ownership === "workspace"
|
|
1266
|
+
? "workspace:admin"
|
|
1267
|
+
: "workspace:read"
|
|
1268
|
+
: input.providerId === "github-app"
|
|
1269
|
+
? "github:manage"
|
|
1270
|
+
: input.providerId === "github-lens"
|
|
1271
|
+
? "workspace:admin"
|
|
1272
|
+
: "connections:write";
|
|
1273
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1274
|
+
c,
|
|
1275
|
+
deps,
|
|
1276
|
+
workspaceId,
|
|
1277
|
+
setupPermission,
|
|
1278
|
+
);
|
|
1279
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
1280
|
+
if (!authorization.contextIntegrity)
|
|
1281
|
+
throw new HTTPException(403, {
|
|
1282
|
+
message: "Verified Connect authority required",
|
|
1283
|
+
});
|
|
1284
|
+
let target: URL;
|
|
1285
|
+
try {
|
|
1286
|
+
target = new URL(input.returnUrl);
|
|
1287
|
+
} catch {
|
|
1288
|
+
throw new HTTPException(400, { message: "invalid return URL" });
|
|
1289
|
+
}
|
|
1290
|
+
if (
|
|
1291
|
+
!["https:", "http:"].includes(target.protocol) ||
|
|
1292
|
+
target.username ||
|
|
1293
|
+
target.password ||
|
|
1294
|
+
/[\u0000-\u0020\u007f]/.test(input.returnUrl)
|
|
1295
|
+
)
|
|
1296
|
+
throw new HTTPException(400, { message: "invalid return URL" });
|
|
1297
|
+
const scope = {
|
|
1298
|
+
accountId: authorization.grant.accountId,
|
|
1299
|
+
workspaceId,
|
|
1300
|
+
subjectId: authorization.grant.subjectId,
|
|
1301
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
|
|
1302
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1303
|
+
};
|
|
1304
|
+
return c.json(
|
|
1305
|
+
await withWorkspaceSubjectRls(deps.db, workspaceId, scope.subjectId, async (tx) => {
|
|
1306
|
+
await requireConnectOwnerAuthority(tx, scope, setupPermission);
|
|
1307
|
+
const id = randomUUID();
|
|
1308
|
+
if (input.providerId === "mcp-install") {
|
|
1309
|
+
if (
|
|
1310
|
+
input.ownership !== "workspace" ||
|
|
1311
|
+
input.reconnectAccountId ||
|
|
1312
|
+
input.installationTarget
|
|
1313
|
+
)
|
|
1314
|
+
throw new HTTPException(422, {
|
|
1315
|
+
message: "MCP capabilities are workspace installations, not credential accounts",
|
|
1316
|
+
});
|
|
1317
|
+
const catalog = await buildCapabilityCatalog({
|
|
1318
|
+
db: tx,
|
|
1319
|
+
workspaceId,
|
|
1320
|
+
settings: deps.settings,
|
|
1321
|
+
subjectId: scope.subjectId,
|
|
1322
|
+
});
|
|
1323
|
+
const options = catalog.items
|
|
1324
|
+
.filter(
|
|
1325
|
+
(item) =>
|
|
1326
|
+
item.kind === "mcp" &&
|
|
1327
|
+
item.surfaceType !== "codex_apps" &&
|
|
1328
|
+
item.runtime.available &&
|
|
1329
|
+
item.authKind === "none",
|
|
1330
|
+
)
|
|
1331
|
+
.map((item) => ({ value: item.id, label: item.name }));
|
|
1332
|
+
if (options.length > 1000)
|
|
1333
|
+
throw new HTTPException(422, {
|
|
1334
|
+
message: "MCP catalog exceeds the setup chooser limit",
|
|
1335
|
+
});
|
|
1336
|
+
return beginConnectAttempt(tx, scope, {
|
|
1337
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1338
|
+
idempotencyKey: input.idempotencyKey,
|
|
1339
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1340
|
+
returnUrl: input.returnUrl,
|
|
1341
|
+
attempt: {
|
|
1342
|
+
id,
|
|
1343
|
+
workspaceId,
|
|
1344
|
+
providerId: input.providerId,
|
|
1345
|
+
ownership: "workspace",
|
|
1346
|
+
revision: 1,
|
|
1347
|
+
state: "credential_input",
|
|
1348
|
+
credentialsCommitted: false,
|
|
1349
|
+
integrationInstalled: false,
|
|
1350
|
+
completionRequirement: "provider_setup",
|
|
1351
|
+
nextAction: {
|
|
1352
|
+
type: "credentials",
|
|
1353
|
+
fields: [
|
|
1354
|
+
{
|
|
1355
|
+
name: "capabilityId",
|
|
1356
|
+
label: "MCP capability",
|
|
1357
|
+
required: true,
|
|
1358
|
+
secret: false,
|
|
1359
|
+
options,
|
|
1360
|
+
},
|
|
1361
|
+
],
|
|
1362
|
+
},
|
|
1363
|
+
expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
if (input.providerId === "x" || input.providerId === "reddit") {
|
|
1368
|
+
if (input.installationTarget)
|
|
1369
|
+
throw new HTTPException(422, {
|
|
1370
|
+
message: "Social setup does not create an API installation",
|
|
1371
|
+
});
|
|
1372
|
+
if (input.ownership === "personal" && !scope.personalOwnerVerified)
|
|
1373
|
+
throw new HTTPException(403, { message: "Personal ownership unavailable" });
|
|
1374
|
+
const reconnectId = input.reconnectAccountId?.startsWith("social:")
|
|
1375
|
+
? input.reconnectAccountId.slice("social:".length)
|
|
1376
|
+
: null;
|
|
1377
|
+
const existing =
|
|
1378
|
+
reconnectId && z.string().uuid().safeParse(reconnectId).success
|
|
1379
|
+
? await getSocialConnection(tx, workspaceId, reconnectId, scope.subjectId)
|
|
1380
|
+
: null;
|
|
1381
|
+
if (
|
|
1382
|
+
input.reconnectAccountId &&
|
|
1383
|
+
(!existing ||
|
|
1384
|
+
existing.provider !== input.providerId ||
|
|
1385
|
+
existing.ownership !== input.ownership ||
|
|
1386
|
+
!existing.version ||
|
|
1387
|
+
!existing.externalAccountId)
|
|
1388
|
+
)
|
|
1389
|
+
throw new HTTPException(409, {
|
|
1390
|
+
message: "Social account changed or cannot be safely reconnected",
|
|
1391
|
+
});
|
|
1392
|
+
const started = await startSocialOAuth(
|
|
1393
|
+
{ db: tx, settings: deps.settings, observability: deps.observability },
|
|
1394
|
+
{
|
|
1395
|
+
accountId: scope.accountId,
|
|
1396
|
+
workspaceId,
|
|
1397
|
+
subjectId: scope.subjectId,
|
|
1398
|
+
personalOwnershipAllowed: scope.personalOwnerVerified,
|
|
1399
|
+
connectAttemptId: id,
|
|
1400
|
+
requestUrl: c.req.url,
|
|
1401
|
+
payload: { provider: input.providerId, ownership: input.ownership },
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1404
|
+
if (!started.authorizationUrl)
|
|
1405
|
+
throw new HTTPException(503, { message: "Social authorization is unavailable" });
|
|
1406
|
+
return beginConnectAttempt(tx, scope, {
|
|
1407
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1408
|
+
idempotencyKey: input.idempotencyKey,
|
|
1409
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1410
|
+
returnUrl: input.returnUrl,
|
|
1411
|
+
attempt: {
|
|
1412
|
+
id,
|
|
1413
|
+
workspaceId,
|
|
1414
|
+
providerId: input.providerId,
|
|
1415
|
+
ownership: input.ownership,
|
|
1416
|
+
revision: 1,
|
|
1417
|
+
state: "requires_user_action",
|
|
1418
|
+
credentialsCommitted: false,
|
|
1419
|
+
integrationInstalled: false,
|
|
1420
|
+
completionRequirement: "connection",
|
|
1421
|
+
...(existing
|
|
1422
|
+
? {
|
|
1423
|
+
account: {
|
|
1424
|
+
id: `social:${existing.id}`,
|
|
1425
|
+
version: existing.version!,
|
|
1426
|
+
providerId: input.providerId,
|
|
1427
|
+
label: existing.accountHandle,
|
|
1428
|
+
ownership: input.ownership,
|
|
1429
|
+
status: "auth_needed" as const,
|
|
1430
|
+
},
|
|
1431
|
+
}
|
|
1432
|
+
: {}),
|
|
1433
|
+
nextAction: { type: "authorize", url: started.authorizationUrl },
|
|
1434
|
+
expiresAt: started.expiresAt,
|
|
1435
|
+
},
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
if (["github-app", "github-lens"].includes(input.providerId)) {
|
|
1439
|
+
if (
|
|
1440
|
+
input.ownership !== "workspace" ||
|
|
1441
|
+
input.installationTarget ||
|
|
1442
|
+
input.reconnectAccountId
|
|
1443
|
+
)
|
|
1444
|
+
throw new HTTPException(422, {
|
|
1445
|
+
message:
|
|
1446
|
+
"GitHub App setup requires workspace ownership and explicit installation selection",
|
|
1447
|
+
});
|
|
1448
|
+
if (input.providerId === "github-lens") {
|
|
1449
|
+
if (!hasPermission(authorization.grant.permissions, "secrets:write"))
|
|
1450
|
+
throw new HTTPException(403, { message: "secrets:write required" });
|
|
1451
|
+
await requireConnectOwnerAuthority(tx, scope, "secrets:write");
|
|
1452
|
+
await requireGitHubLensConnect({ ...deps, db: tx }, workspaceId);
|
|
1453
|
+
}
|
|
1454
|
+
const navigation = githubAppConnectNavigation(
|
|
1455
|
+
deps,
|
|
1456
|
+
scope,
|
|
1457
|
+
id,
|
|
1458
|
+
c.req.url,
|
|
1459
|
+
"discover",
|
|
1460
|
+
undefined,
|
|
1461
|
+
input.providerId === "github-lens" ? "github-lens" : "github-app",
|
|
1462
|
+
);
|
|
1463
|
+
return beginConnectAttempt(tx, scope, {
|
|
1464
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1465
|
+
idempotencyKey: input.idempotencyKey,
|
|
1466
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1467
|
+
returnUrl: input.returnUrl,
|
|
1468
|
+
attempt: {
|
|
1469
|
+
id,
|
|
1470
|
+
workspaceId,
|
|
1471
|
+
providerId: input.providerId,
|
|
1472
|
+
ownership: "workspace",
|
|
1473
|
+
revision: 1,
|
|
1474
|
+
state: "requires_user_action",
|
|
1475
|
+
credentialsCommitted: false,
|
|
1476
|
+
integrationInstalled: false,
|
|
1477
|
+
completionRequirement: "provider_setup",
|
|
1478
|
+
nextAction: { type: "authorize", url: navigation.authorizationUrl },
|
|
1479
|
+
expiresAt: navigation.expiresAt,
|
|
1480
|
+
},
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (["openapi", "graphql"].includes(input.providerId)) {
|
|
1484
|
+
if (input.reconnectAccountId)
|
|
1485
|
+
throw new HTTPException(422, { message: "Select the service connection during setup" });
|
|
1486
|
+
if (input.ownership === "personal" && !isPersonalConnectionOwnerPrincipal(authorization))
|
|
1487
|
+
throw new HTTPException(403, { message: "personal ownership unavailable" });
|
|
1488
|
+
const choices = hasPermission(authorization.grant.permissions, "connections:read")
|
|
1489
|
+
? (await listConnectionsMetadata(tx, workspaceId, scope.subjectId))
|
|
1490
|
+
.filter(
|
|
1491
|
+
(connection) =>
|
|
1492
|
+
connection.status === "active" &&
|
|
1493
|
+
(input.ownership === "workspace"
|
|
1494
|
+
? connection.subjectId === null
|
|
1495
|
+
: connection.subjectId === scope.subjectId),
|
|
1496
|
+
)
|
|
1497
|
+
.map((connection) => ({
|
|
1498
|
+
value: connection.id,
|
|
1499
|
+
label: `${connection.providerDomain} · ${connection.id}`,
|
|
1500
|
+
}))
|
|
1501
|
+
: [];
|
|
1502
|
+
if (choices.length > 1000)
|
|
1503
|
+
throw new HTTPException(422, {
|
|
1504
|
+
message: "Account inventory exceeds the setup chooser limit",
|
|
1505
|
+
});
|
|
1506
|
+
return beginConnectAttempt(tx, scope, {
|
|
1507
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1508
|
+
idempotencyKey: input.idempotencyKey,
|
|
1509
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1510
|
+
returnUrl: input.returnUrl,
|
|
1511
|
+
attempt: {
|
|
1512
|
+
id,
|
|
1513
|
+
workspaceId,
|
|
1514
|
+
providerId: input.providerId,
|
|
1515
|
+
ownership: input.ownership,
|
|
1516
|
+
revision: 1,
|
|
1517
|
+
state: "credential_input",
|
|
1518
|
+
credentialsCommitted: false,
|
|
1519
|
+
integrationInstalled: false,
|
|
1520
|
+
completionRequirement: "integration",
|
|
1521
|
+
installationTarget: input.installationTarget ?? {
|
|
1522
|
+
instanceKey: `account-${id}`,
|
|
1523
|
+
displayName: input.providerId === "openapi" ? "OpenAPI service" : "GraphQL service",
|
|
1524
|
+
},
|
|
1525
|
+
nextAction: {
|
|
1526
|
+
type: "credentials",
|
|
1527
|
+
fields: [
|
|
1528
|
+
{
|
|
1529
|
+
name: "url",
|
|
1530
|
+
label:
|
|
1531
|
+
input.providerId === "openapi"
|
|
1532
|
+
? "OpenAPI document URL"
|
|
1533
|
+
: "GraphQL endpoint URL",
|
|
1534
|
+
required: true,
|
|
1535
|
+
secret: false,
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
name: "connectionId",
|
|
1539
|
+
label: "Service account (optional for public services)",
|
|
1540
|
+
required: false,
|
|
1541
|
+
secret: false,
|
|
1542
|
+
options: choices,
|
|
1543
|
+
},
|
|
1544
|
+
],
|
|
1545
|
+
},
|
|
1546
|
+
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
if (
|
|
1551
|
+
input.installationTarget &&
|
|
1552
|
+
[
|
|
1553
|
+
"mcp-oauth",
|
|
1554
|
+
"slack-personal",
|
|
1555
|
+
"mcp-bearer",
|
|
1556
|
+
"mcp-headers",
|
|
1557
|
+
"fiken-token",
|
|
1558
|
+
"fiken-oauth",
|
|
1559
|
+
"atlassian",
|
|
1560
|
+
"google-drive-knowledge",
|
|
1561
|
+
"google-drive-publish",
|
|
1562
|
+
"slack-bot",
|
|
1563
|
+
"github-personal",
|
|
1564
|
+
].includes(input.providerId)
|
|
1565
|
+
)
|
|
1566
|
+
throw new HTTPException(422, {
|
|
1567
|
+
message: "MCP credential setup does not install an API integration instance",
|
|
1568
|
+
});
|
|
1569
|
+
if (
|
|
1570
|
+
["mcp-oauth", "slack-personal", "mcp-bearer", "mcp-headers", "fiken-token"].includes(
|
|
1571
|
+
input.providerId,
|
|
1572
|
+
)
|
|
1573
|
+
) {
|
|
1574
|
+
if (input.providerId === "slack-personal" && input.ownership !== "personal")
|
|
1575
|
+
throw new HTTPException(422, {
|
|
1576
|
+
message: "Hosted Slack MCP requires personal ownership",
|
|
1577
|
+
});
|
|
1578
|
+
if (input.providerId === "fiken-token" && input.ownership !== "workspace")
|
|
1579
|
+
throw new HTTPException(422, { message: "Fiken is workspace-owned" });
|
|
1580
|
+
requireEnvironmentEncryption(deps.settings);
|
|
1581
|
+
if (["mcp-oauth", "slack-personal"].includes(input.providerId))
|
|
1582
|
+
requireIntegrationsStateSecret(deps.settings);
|
|
1583
|
+
const existing = input.reconnectAccountId
|
|
1584
|
+
? await getConnectionMetadata(
|
|
1585
|
+
tx,
|
|
1586
|
+
workspaceId,
|
|
1587
|
+
input.reconnectAccountId,
|
|
1588
|
+
scope.subjectId,
|
|
1589
|
+
)
|
|
1590
|
+
: null;
|
|
1591
|
+
if (
|
|
1592
|
+
input.reconnectAccountId &&
|
|
1593
|
+
(!existing ||
|
|
1594
|
+
(input.providerId === "fiken-token"
|
|
1595
|
+
? !isFikenConnection(existing)
|
|
1596
|
+
: ["mcp-oauth", "slack-personal"].includes(input.providerId)
|
|
1597
|
+
? existing.kind !== "oauth2" ||
|
|
1598
|
+
!existing.metadata.oauthDiscovery ||
|
|
1599
|
+
(input.providerId === "slack-personal" &&
|
|
1600
|
+
existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL)
|
|
1601
|
+
: existing.kind !== "api_key" ||
|
|
1602
|
+
existing.metadata.connectAdapter !== input.providerId) ||
|
|
1603
|
+
(existing.subjectId === null ? "workspace" : "personal") !== input.ownership)
|
|
1604
|
+
)
|
|
1605
|
+
throw new HTTPException(404, { message: "MCP account not found for this ownership" });
|
|
1606
|
+
if (input.ownership === "personal" && !isPersonalConnectionOwnerPrincipal(authorization))
|
|
1607
|
+
throw new HTTPException(403, { message: "personal ownership unavailable" });
|
|
1608
|
+
return beginConnectAttempt(tx, scope, {
|
|
1609
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1610
|
+
idempotencyKey: input.idempotencyKey,
|
|
1611
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1612
|
+
returnUrl: input.returnUrl,
|
|
1613
|
+
attempt: {
|
|
1614
|
+
id,
|
|
1615
|
+
workspaceId,
|
|
1616
|
+
providerId: input.providerId,
|
|
1617
|
+
ownership: input.ownership,
|
|
1618
|
+
revision: 1,
|
|
1619
|
+
state: "credential_input",
|
|
1620
|
+
credentialsCommitted: false,
|
|
1621
|
+
integrationInstalled: false,
|
|
1622
|
+
completionRequirement: "connection",
|
|
1623
|
+
...(existing
|
|
1624
|
+
? {
|
|
1625
|
+
account: {
|
|
1626
|
+
id: existing.id,
|
|
1627
|
+
version: existing.version,
|
|
1628
|
+
providerId: input.providerId,
|
|
1629
|
+
label: existing.providerDomain,
|
|
1630
|
+
ownership: input.ownership,
|
|
1631
|
+
status: "auth_needed" as const,
|
|
1632
|
+
},
|
|
1633
|
+
}
|
|
1634
|
+
: {}),
|
|
1635
|
+
nextAction: {
|
|
1636
|
+
type: "credentials",
|
|
1637
|
+
fields:
|
|
1638
|
+
input.providerId === "slack-personal"
|
|
1639
|
+
? []
|
|
1640
|
+
: input.providerId === "fiken-token"
|
|
1641
|
+
? [
|
|
1642
|
+
{
|
|
1643
|
+
name: "apiToken",
|
|
1644
|
+
label: "Fiken API token",
|
|
1645
|
+
required: true,
|
|
1646
|
+
secret: true,
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "defaultCompanySlug",
|
|
1650
|
+
label: "Default company slug (optional)",
|
|
1651
|
+
required: false,
|
|
1652
|
+
secret: false,
|
|
1653
|
+
},
|
|
1654
|
+
]
|
|
1655
|
+
: [
|
|
1656
|
+
{
|
|
1657
|
+
name: "mcpUrl",
|
|
1658
|
+
label: "MCP server URL",
|
|
1659
|
+
required: true,
|
|
1660
|
+
secret: false,
|
|
1661
|
+
},
|
|
1662
|
+
...(input.providerId === "mcp-bearer"
|
|
1663
|
+
? [
|
|
1664
|
+
{
|
|
1665
|
+
name: "token",
|
|
1666
|
+
label: "Bearer credential",
|
|
1667
|
+
required: true,
|
|
1668
|
+
secret: true,
|
|
1669
|
+
},
|
|
1670
|
+
]
|
|
1671
|
+
: input.providerId === "mcp-headers"
|
|
1672
|
+
? [
|
|
1673
|
+
{
|
|
1674
|
+
name: "headers",
|
|
1675
|
+
label: "Credential headers (JSON)",
|
|
1676
|
+
required: true,
|
|
1677
|
+
secret: true,
|
|
1678
|
+
},
|
|
1679
|
+
]
|
|
1680
|
+
: []),
|
|
1681
|
+
],
|
|
1682
|
+
},
|
|
1683
|
+
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
1684
|
+
},
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
if (
|
|
1688
|
+
["fiken-oauth", "slack-bot"].includes(input.providerId) &&
|
|
1689
|
+
input.ownership !== "workspace"
|
|
1690
|
+
)
|
|
1691
|
+
throw new HTTPException(422, { message: "This connector is workspace-owned" });
|
|
1692
|
+
if (
|
|
1693
|
+
[
|
|
1694
|
+
"atlassian",
|
|
1695
|
+
"google-drive-knowledge",
|
|
1696
|
+
"google-drive-publish",
|
|
1697
|
+
"github-personal",
|
|
1698
|
+
].includes(input.providerId) &&
|
|
1699
|
+
(input.ownership !== "personal" || !isPersonalConnectionOwnerPrincipal(authorization))
|
|
1700
|
+
)
|
|
1701
|
+
throw new HTTPException(422, { message: "This connector requires personal ownership" });
|
|
1702
|
+
const started =
|
|
1703
|
+
input.providerId === "github-personal"
|
|
1704
|
+
? await startPersonalGitHubOAuth(
|
|
1705
|
+
{ ...deps, db: tx },
|
|
1706
|
+
{
|
|
1707
|
+
access: authorization,
|
|
1708
|
+
workspaceId,
|
|
1709
|
+
connectAttemptId: id,
|
|
1710
|
+
...(input.reconnectAccountId ? { connectionId: input.reconnectAccountId } : {}),
|
|
1711
|
+
},
|
|
1712
|
+
)
|
|
1713
|
+
: input.providerId === "slack-bot"
|
|
1714
|
+
? await startSlackBotInstall(
|
|
1715
|
+
{ ...deps, db: tx },
|
|
1716
|
+
{
|
|
1717
|
+
...scope,
|
|
1718
|
+
requestUrl: c.req.url,
|
|
1719
|
+
connectAttemptId: id,
|
|
1720
|
+
...(input.reconnectAccountId ? { connectionId: input.reconnectAccountId } : {}),
|
|
1721
|
+
},
|
|
1722
|
+
)
|
|
1723
|
+
: ["google-drive-knowledge", "google-drive-publish"].includes(input.providerId)
|
|
1724
|
+
? await startGoogleDriveOAuth(
|
|
1725
|
+
{ ...deps, db: tx },
|
|
1726
|
+
{
|
|
1727
|
+
...scope,
|
|
1728
|
+
requestUrl: c.req.url,
|
|
1729
|
+
connectAttemptId: id,
|
|
1730
|
+
payload: {
|
|
1731
|
+
capability:
|
|
1732
|
+
input.providerId === "google-drive-publish" ? "publish" : "source_read",
|
|
1733
|
+
...(input.reconnectAccountId
|
|
1734
|
+
? { connectionId: input.reconnectAccountId }
|
|
1735
|
+
: {}),
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
)
|
|
1739
|
+
: input.providerId === "atlassian"
|
|
1740
|
+
? await startAtlassianOAuth(
|
|
1741
|
+
{ ...deps, db: tx },
|
|
1742
|
+
{
|
|
1743
|
+
...scope,
|
|
1744
|
+
requestUrl: c.req.url,
|
|
1745
|
+
connectAttemptId: id,
|
|
1746
|
+
payload: input.reconnectAccountId
|
|
1747
|
+
? { connectionId: input.reconnectAccountId }
|
|
1748
|
+
: {},
|
|
1749
|
+
},
|
|
1750
|
+
)
|
|
1751
|
+
: input.providerId === "fiken-oauth"
|
|
1752
|
+
? await startFikenOAuth(
|
|
1753
|
+
{ ...deps, db: tx },
|
|
1754
|
+
{
|
|
1755
|
+
...scope,
|
|
1756
|
+
requestUrl: c.req.url,
|
|
1757
|
+
connectAttemptId: id,
|
|
1758
|
+
payload: input.reconnectAccountId
|
|
1759
|
+
? { connectionId: input.reconnectAccountId }
|
|
1760
|
+
: {},
|
|
1761
|
+
},
|
|
1762
|
+
)
|
|
1763
|
+
: await startApiIntegrationProviderOAuth(
|
|
1764
|
+
{ ...deps, db: tx },
|
|
1765
|
+
{
|
|
1766
|
+
...scope,
|
|
1767
|
+
personalOwnershipAllowed:
|
|
1768
|
+
isPersonalConnectionOwnerPrincipal(authorization),
|
|
1769
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1770
|
+
connectAttemptId: id,
|
|
1771
|
+
requestUrl: c.req.url,
|
|
1772
|
+
payload: {
|
|
1773
|
+
definitionId: input.providerId,
|
|
1774
|
+
ownership: input.ownership,
|
|
1775
|
+
...(input.reconnectAccountId
|
|
1776
|
+
? { connectionId: input.reconnectAccountId }
|
|
1777
|
+
: {}),
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
);
|
|
1781
|
+
if (!started.authorizationUrl)
|
|
1782
|
+
throw new HTTPException(502, { message: "provider did not return an authorization URL" });
|
|
1783
|
+
const reconnect = input.reconnectAccountId
|
|
1784
|
+
? await getConnectionMetadata(tx, workspaceId, input.reconnectAccountId, scope.subjectId)
|
|
1785
|
+
: null;
|
|
1786
|
+
return beginConnectAttempt(tx, scope, {
|
|
1787
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1788
|
+
idempotencyKey: input.idempotencyKey,
|
|
1789
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1790
|
+
returnUrl: input.returnUrl,
|
|
1791
|
+
attempt: {
|
|
1792
|
+
id,
|
|
1793
|
+
workspaceId,
|
|
1794
|
+
providerId: input.providerId,
|
|
1795
|
+
ownership: input.ownership,
|
|
1796
|
+
revision: 1,
|
|
1797
|
+
state: "requires_user_action",
|
|
1798
|
+
credentialsCommitted: false,
|
|
1799
|
+
integrationInstalled: false,
|
|
1800
|
+
completionRequirement: [
|
|
1801
|
+
"github-personal",
|
|
1802
|
+
"slack-bot",
|
|
1803
|
+
"fiken-oauth",
|
|
1804
|
+
"atlassian",
|
|
1805
|
+
"google-drive-knowledge",
|
|
1806
|
+
"google-drive-publish",
|
|
1807
|
+
].includes(input.providerId)
|
|
1808
|
+
? "connection"
|
|
1809
|
+
: "integration",
|
|
1810
|
+
...([
|
|
1811
|
+
"github-personal",
|
|
1812
|
+
"slack-bot",
|
|
1813
|
+
"fiken-oauth",
|
|
1814
|
+
"atlassian",
|
|
1815
|
+
"google-drive-knowledge",
|
|
1816
|
+
"google-drive-publish",
|
|
1817
|
+
].includes(input.providerId)
|
|
1818
|
+
? {}
|
|
1819
|
+
: {
|
|
1820
|
+
installationTarget: input.installationTarget ?? {
|
|
1821
|
+
instanceKey: `account-${id}`,
|
|
1822
|
+
displayName: input.providerId,
|
|
1823
|
+
},
|
|
1824
|
+
}),
|
|
1825
|
+
...(reconnect
|
|
1826
|
+
? {
|
|
1827
|
+
account: {
|
|
1828
|
+
id: reconnect.id,
|
|
1829
|
+
version: reconnect.version,
|
|
1830
|
+
providerId: input.providerId,
|
|
1831
|
+
label: reconnect.providerDomain,
|
|
1832
|
+
ownership: input.ownership,
|
|
1833
|
+
status: "auth_needed" as const,
|
|
1834
|
+
},
|
|
1835
|
+
}
|
|
1836
|
+
: {}),
|
|
1837
|
+
nextAction: { type: "authorize", url: started.authorizationUrl },
|
|
1838
|
+
expiresAt: started.expiresAt,
|
|
1839
|
+
},
|
|
1840
|
+
});
|
|
1841
|
+
}),
|
|
1842
|
+
);
|
|
1843
|
+
});
|
|
1844
|
+
app.get("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
|
|
1845
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1846
|
+
const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1847
|
+
return c.json(
|
|
1848
|
+
(
|
|
1849
|
+
await listPendingConnectAttempts(deps.db, {
|
|
1850
|
+
accountId: grant.accountId,
|
|
1851
|
+
workspaceId,
|
|
1852
|
+
subjectId: grant.subjectId,
|
|
1853
|
+
})
|
|
1854
|
+
).filter((attempt) => canReadAttempt(grant.permissions, attempt.providerId)),
|
|
1855
|
+
);
|
|
1856
|
+
});
|
|
1857
|
+
app.get("/v1/workspaces/:workspaceId/connect/attempts/:attemptId", async (c) => {
|
|
1858
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1859
|
+
const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1860
|
+
const { attempt } = await getConnectAttempt(
|
|
1861
|
+
deps.db,
|
|
1862
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1863
|
+
c.req.param("attemptId"),
|
|
1864
|
+
);
|
|
1865
|
+
if (!canReadAttempt(grant.permissions, attempt.providerId))
|
|
1866
|
+
throw new HTTPException(403, { message: "Connect read permission required" });
|
|
1867
|
+
return c.json(attempt);
|
|
1868
|
+
});
|
|
1869
|
+
}
|