@opengeni/api-router 3.0.0-canary.0 → 3.0.1-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-7IAWEYBH.js → chunk-XZGTKYOA.js} +3626 -2525
- package/dist/{chunk-7IAWEYBH.js.map → chunk-XZGTKYOA.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +2 -2
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +118 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/package.json +19 -19
- package/src/app.ts +7 -2
- package/src/integrations/atlassian.ts +135 -65
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +34 -15
- package/src/integrations/google-drive.ts +295 -124
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +220 -65
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +54 -31
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +23 -6
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/billing.ts +52 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/connect.ts +161 -57
- package/src/routes/connections.ts +430 -305
- package/src/routes/github.ts +48 -19
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/plugins.ts +12 -2
- package/src/routes/pr-review-github.ts +60 -30
- package/src/routes/pr-review.ts +69 -45
- package/src/routes/social.ts +24 -13
package/src/routes/billing.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CreateBillingPortalResponse,
|
|
4
4
|
CreateCheckoutRequest,
|
|
5
5
|
CreateCheckoutResponse,
|
|
6
|
+
OrganizationUsageQuery,
|
|
7
|
+
OrganizationUsageWorkspacePageQuery,
|
|
6
8
|
type AccessContext,
|
|
7
9
|
type Permission,
|
|
8
10
|
} from "@opengeni/contracts";
|
|
@@ -14,6 +16,9 @@ import {
|
|
|
14
16
|
hasCreditLedgerEntry,
|
|
15
17
|
isStripeWebhookProcessed,
|
|
16
18
|
listUsageEvents,
|
|
19
|
+
getOrganizationUsageSummary,
|
|
20
|
+
getOrganizationUsageWorkspacePage,
|
|
21
|
+
withSessionRlsActorContext,
|
|
17
22
|
getManagedAccount,
|
|
18
23
|
markStripeWebhookProcessed,
|
|
19
24
|
recordStripeWebhookEvent,
|
|
@@ -24,6 +29,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
24
29
|
import Stripe from "stripe";
|
|
25
30
|
import { requireAccessContext } from "@opengeni/core";
|
|
26
31
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
32
|
+
import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
|
|
27
33
|
|
|
28
34
|
export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
29
35
|
app.get("/v1/billing", async (c) => {
|
|
@@ -57,6 +63,33 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
57
63
|
});
|
|
58
64
|
});
|
|
59
65
|
|
|
66
|
+
app.get("/v1/billing/usage-summary", async (c) => {
|
|
67
|
+
const context = await requireAccessContext(c, deps);
|
|
68
|
+
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
69
|
+
const parsed = OrganizationUsageQuery.safeParse(c.req.query());
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
throw new HTTPException(400, {
|
|
72
|
+
message: parsed.error.issues[0]?.message ?? "invalid usage query",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return await withBillingUsageActor(deps, context, accountId, async () =>
|
|
76
|
+
c.json(await getOrganizationUsageSummary(deps.db, { accountId, ...parsed.data })),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.get("/v1/billing/usage-workspaces", async (c) => {
|
|
81
|
+
const context = await requireAccessContext(c, deps);
|
|
82
|
+
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
83
|
+
const parsed = OrganizationUsageWorkspacePageQuery.safeParse(c.req.query());
|
|
84
|
+
if (!parsed.success)
|
|
85
|
+
throw new HTTPException(400, {
|
|
86
|
+
message: parsed.error.issues[0]?.message ?? "invalid usage page query",
|
|
87
|
+
});
|
|
88
|
+
return await withBillingUsageActor(deps, context, accountId, async () =>
|
|
89
|
+
c.json(await getOrganizationUsageWorkspacePage(deps.db, { accountId, ...parsed.data })),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
60
93
|
app.get("/v1/billing/entitlements", async (c) => {
|
|
61
94
|
const context = await requireAccessContext(c, deps);
|
|
62
95
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
@@ -638,6 +671,25 @@ export function stripeCustomerProvider(
|
|
|
638
671
|
: "stripe:test";
|
|
639
672
|
}
|
|
640
673
|
|
|
674
|
+
/** Billing routes do not traverse the workspace actor middleware. Always bind
|
|
675
|
+
* these reads explicitly; only a revalidated live attempt may supply a human
|
|
676
|
+
* initiator. Neither query parameters nor serviceInitiator claims are proof. */
|
|
677
|
+
async function withBillingUsageActor<T>(
|
|
678
|
+
deps: ApiRouteDeps,
|
|
679
|
+
context: AccessContext,
|
|
680
|
+
accountId: string,
|
|
681
|
+
read: () => Promise<T>,
|
|
682
|
+
): Promise<T> {
|
|
683
|
+
const attemptGrant = context.workspaceGrants.find(
|
|
684
|
+
(grant) =>
|
|
685
|
+
grant.accountId === accountId &&
|
|
686
|
+
grant.subjectId === context.subjectId &&
|
|
687
|
+
grant.principalKind === "agent_attempt",
|
|
688
|
+
);
|
|
689
|
+
if (attemptGrant) return await withAccessGrantSessionRlsContext(deps, attemptGrant, read);
|
|
690
|
+
return await withSessionRlsActorContext({ subjectId: context.subjectId }, read);
|
|
691
|
+
}
|
|
692
|
+
|
|
641
693
|
function requireSelectedAccount(
|
|
642
694
|
context: AccessContext,
|
|
643
695
|
requested: string | undefined,
|
|
@@ -3,9 +3,16 @@ import {
|
|
|
3
3
|
CreateCapabilityCatalogItemRequest,
|
|
4
4
|
DiscoverMcpCapabilitiesResponse,
|
|
5
5
|
EnableCapabilityRequest,
|
|
6
|
+
ConnectorToolPermissionsResponse,
|
|
7
|
+
UpdateConnectorToolPermissionsRequest,
|
|
6
8
|
} from "@opengeni/contracts";
|
|
7
9
|
import type { Hono } from "hono";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
requireAccessGrant,
|
|
12
|
+
requireAccessGrantAuthorization,
|
|
13
|
+
getConnectorToolPermissions,
|
|
14
|
+
updateConnectorToolPermissions,
|
|
15
|
+
} from "@opengeni/core";
|
|
9
16
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
10
17
|
import {
|
|
11
18
|
buildCapabilityCatalog,
|
|
@@ -15,6 +22,7 @@ import {
|
|
|
15
22
|
enableCapability,
|
|
16
23
|
officialMcpRegistryUrl,
|
|
17
24
|
} from "@opengeni/core";
|
|
25
|
+
import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
|
|
18
26
|
import { boundedLimit } from "../http/common";
|
|
19
27
|
import { z } from "zod";
|
|
20
28
|
import pluginSnapshot from "../../../../data/catalog/plugins-snapshot.json";
|
|
@@ -110,6 +118,48 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
110
118
|
);
|
|
111
119
|
});
|
|
112
120
|
|
|
121
|
+
app.get("/v1/workspaces/:workspaceId/capabilities/:capabilityId/tool-permissions", async (c) => {
|
|
122
|
+
const workspaceId = c.req.param("workspaceId");
|
|
123
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
|
|
124
|
+
return c.json(
|
|
125
|
+
ConnectorToolPermissionsResponse.parse(
|
|
126
|
+
await getConnectorToolPermissions({
|
|
127
|
+
db,
|
|
128
|
+
settings,
|
|
129
|
+
workspaceId,
|
|
130
|
+
grant: access.grant,
|
|
131
|
+
capabilityId: decodeURIComponent(c.req.param("capabilityId")),
|
|
132
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(access),
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.patch(
|
|
139
|
+
"/v1/workspaces/:workspaceId/capabilities/:capabilityId/tool-permissions",
|
|
140
|
+
async (c) => {
|
|
141
|
+
const workspaceId = c.req.param("workspaceId");
|
|
142
|
+
const access = await requireAccessGrantAuthorization(
|
|
143
|
+
c,
|
|
144
|
+
deps,
|
|
145
|
+
workspaceId,
|
|
146
|
+
"capabilities:manage",
|
|
147
|
+
);
|
|
148
|
+
const payload = UpdateConnectorToolPermissionsRequest.safeParse(await c.req.json());
|
|
149
|
+
if (!payload.success) return c.json({ error: "Invalid connector permission target" }, 400);
|
|
150
|
+
await updateConnectorToolPermissions({
|
|
151
|
+
db,
|
|
152
|
+
settings,
|
|
153
|
+
workspaceId,
|
|
154
|
+
grant: access.grant,
|
|
155
|
+
capabilityId: decodeURIComponent(c.req.param("capabilityId")),
|
|
156
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(access),
|
|
157
|
+
payload: payload.data,
|
|
158
|
+
});
|
|
159
|
+
return c.json({ saved: true });
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
113
163
|
app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
|
|
114
164
|
const workspaceId = c.req.param("workspaceId");
|
|
115
165
|
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
package/src/routes/connect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
2
2
|
import type { Hono } from "hono";
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { getConnectBeginReplay } from "@opengeni/db/connect-attempts";
|
|
4
5
|
import {
|
|
5
6
|
AdvanceConnectRequest,
|
|
6
7
|
BeginConnectRequest,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
13
14
|
InstallApiIntegrationRequest,
|
|
14
15
|
stableJson,
|
|
16
|
+
assertOrganizationIntegrationAllowed,
|
|
15
17
|
OPENGENI_PERSONAL_SLACK_MCP_URL,
|
|
16
18
|
OPENGENI_PR_REVIEW_PACK_ID,
|
|
17
19
|
} from "@opengeni/contracts";
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
createPinnedIntegrationTransport,
|
|
21
23
|
} from "@opengeni/capabilities";
|
|
22
24
|
import {
|
|
23
|
-
beginConnectAttempt,
|
|
25
|
+
beginConnectAttempt as persistConnectAttempt,
|
|
24
26
|
getConnectAttempt,
|
|
25
27
|
listPendingConnectAttempts,
|
|
26
28
|
withWorkspaceSubjectRls,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
isOpenGeniSlackBotConnection,
|
|
47
49
|
prepareCapabilityEnable,
|
|
48
50
|
buildCapabilityCatalog,
|
|
51
|
+
integrationKeyForConnectProvider,
|
|
49
52
|
type ApiRouteDeps,
|
|
50
53
|
} from "@opengeni/core";
|
|
51
54
|
import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
|
|
@@ -74,7 +77,9 @@ import {
|
|
|
74
77
|
} from "../integrations/provider-oauth";
|
|
75
78
|
import { resolveForRoute, validatedIntegrationInstallInput } from "./api-integrations";
|
|
76
79
|
import { executeConnectOperation } from "@opengeni/core";
|
|
80
|
+
import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
|
|
77
81
|
import { startMcpOAuth, requireIntegrationsStateSecret } from "../integrations/oauth-client";
|
|
82
|
+
import { OFFICIAL_GMAIL_MCP_URL } from "../integrations/oauth-profiles";
|
|
78
83
|
import { z } from "zod";
|
|
79
84
|
import {
|
|
80
85
|
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
@@ -100,6 +105,11 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
100
105
|
"workspace:read",
|
|
101
106
|
);
|
|
102
107
|
const external = authorization.contextIntegrity;
|
|
108
|
+
const policy = await withOrganizationIntegrationPolicyFence(
|
|
109
|
+
deps.db,
|
|
110
|
+
authorization.grant,
|
|
111
|
+
async (_tx, current) => current,
|
|
112
|
+
);
|
|
103
113
|
const canWrite = hasPermission(authorization.grant.permissions, "connections:write");
|
|
104
114
|
const personal = isPersonalConnectionOwnerPrincipal(authorization);
|
|
105
115
|
const lensPack = await getPackInstallation(
|
|
@@ -117,7 +127,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
117
127
|
} catch {
|
|
118
128
|
/* Missing operator configuration is catalog readiness, not actor authority. */
|
|
119
129
|
}
|
|
120
|
-
|
|
130
|
+
const providers = [
|
|
121
131
|
...(["x", "reddit"] as const).map((providerId) => {
|
|
122
132
|
let configured = mcpConfigured;
|
|
123
133
|
try {
|
|
@@ -227,6 +237,19 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
227
237
|
ownership: ["workspace"],
|
|
228
238
|
setup: ["oauth"],
|
|
229
239
|
}),
|
|
240
|
+
ConnectProvider.parse({
|
|
241
|
+
id: "gmail",
|
|
242
|
+
label: "Gmail",
|
|
243
|
+
family: "google",
|
|
244
|
+
readiness:
|
|
245
|
+
!external || !canWrite || !personal
|
|
246
|
+
? "unsupported"
|
|
247
|
+
: mcpConfigured
|
|
248
|
+
? "available"
|
|
249
|
+
: "needs_configuration",
|
|
250
|
+
ownership: personal ? ["personal"] : [],
|
|
251
|
+
setup: ["oauth"],
|
|
252
|
+
}),
|
|
230
253
|
ConnectProvider.parse({
|
|
231
254
|
id: "slack-personal",
|
|
232
255
|
label: "My Slack account",
|
|
@@ -377,7 +400,16 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
377
400
|
ownership: personal ? ["workspace", "personal"] : ["workspace"],
|
|
378
401
|
setup: ["credentials"],
|
|
379
402
|
}),
|
|
380
|
-
]
|
|
403
|
+
];
|
|
404
|
+
return c.json(
|
|
405
|
+
providers.filter(
|
|
406
|
+
(provider) =>
|
|
407
|
+
policy.mode === "unrestricted" ||
|
|
408
|
+
policy.allowedIntegrationKeys.includes(
|
|
409
|
+
integrationKeyForConnectProvider(provider.id) ?? "",
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
);
|
|
381
413
|
});
|
|
382
414
|
app.get("/v1/workspaces/:workspaceId/connect/accounts", async (c) => {
|
|
383
415
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -557,7 +589,9 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
557
589
|
providerId:
|
|
558
590
|
connection.metadata.mcpUrl === OPENGENI_PERSONAL_SLACK_MCP_URL
|
|
559
591
|
? "slack-personal"
|
|
560
|
-
:
|
|
592
|
+
: connection.metadata.mcpUrl === OFFICIAL_GMAIL_MCP_URL
|
|
593
|
+
? "gmail"
|
|
594
|
+
: "mcp-oauth",
|
|
561
595
|
label: connection.providerDomain,
|
|
562
596
|
ownership: connection.subjectId === null ? "workspace" : "personal",
|
|
563
597
|
status:
|
|
@@ -738,6 +772,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
738
772
|
const resolved = await resolveForRoute({
|
|
739
773
|
deps,
|
|
740
774
|
transport,
|
|
775
|
+
accountId: scope.accountId,
|
|
741
776
|
workspaceId,
|
|
742
777
|
subjectId: scope.subjectId,
|
|
743
778
|
payload: {
|
|
@@ -1007,16 +1042,18 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1007
1042
|
}),
|
|
1008
1043
|
);
|
|
1009
1044
|
}
|
|
1010
|
-
if (["mcp-oauth", "slack-personal"].includes(before.providerId)) {
|
|
1045
|
+
if (["mcp-oauth", "slack-personal", "gmail"].includes(before.providerId)) {
|
|
1011
1046
|
if (action.type !== "credentials")
|
|
1012
1047
|
throw new HTTPException(422, { message: "MCP OAuth requires a server URL" });
|
|
1013
1048
|
const values =
|
|
1014
1049
|
before.providerId === "slack-personal"
|
|
1015
1050
|
? { mcpUrl: OPENGENI_PERSONAL_SLACK_MCP_URL }
|
|
1016
|
-
:
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1051
|
+
: before.providerId === "gmail"
|
|
1052
|
+
? { mcpUrl: OFFICIAL_GMAIL_MCP_URL }
|
|
1053
|
+
: z
|
|
1054
|
+
.object({ mcpUrl: z.string().url().max(4096) })
|
|
1055
|
+
.strict()
|
|
1056
|
+
.parse(action.values);
|
|
1020
1057
|
return c.json(
|
|
1021
1058
|
await executeConnectOperation({
|
|
1022
1059
|
db: deps.db,
|
|
@@ -1034,6 +1071,9 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1034
1071
|
throw new HTTPException(409, { message: "Reload the current connection setup" });
|
|
1035
1072
|
const started = await startMcpOAuth(deps, {
|
|
1036
1073
|
...scope,
|
|
1074
|
+
...(before.providerId === "gmail" || before.providerId === "slack-personal"
|
|
1075
|
+
? { integrationKey: before.providerId }
|
|
1076
|
+
: {}),
|
|
1037
1077
|
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1038
1078
|
connectAttemptId: attempt.id,
|
|
1039
1079
|
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(authorization),
|
|
@@ -1126,6 +1166,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1126
1166
|
const resolved = await resolveForRoute({
|
|
1127
1167
|
deps,
|
|
1128
1168
|
transport,
|
|
1169
|
+
accountId: scope.accountId,
|
|
1129
1170
|
workspaceId,
|
|
1130
1171
|
subjectId: scope.subjectId,
|
|
1131
1172
|
payload: previewRequest,
|
|
@@ -1219,6 +1260,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1219
1260
|
inputDigest: createHash("sha256")
|
|
1220
1261
|
.update(stableJson({ ...input, action: "cancel" }))
|
|
1221
1262
|
.digest("hex"),
|
|
1263
|
+
purpose: "cancellation",
|
|
1222
1264
|
authorize: async (tx, attempt, origin) => {
|
|
1223
1265
|
const permission =
|
|
1224
1266
|
attempt.providerId === "mcp-install"
|
|
@@ -1302,8 +1344,59 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1302
1344
|
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1303
1345
|
};
|
|
1304
1346
|
return c.json(
|
|
1305
|
-
await
|
|
1306
|
-
|
|
1347
|
+
await (async () => {
|
|
1348
|
+
// Remote setup preparation stays outside the policy and actor transactions.
|
|
1349
|
+
// Repeat both admissions when persisting the resulting attempt.
|
|
1350
|
+
const tx = deps.db;
|
|
1351
|
+
const admit = () =>
|
|
1352
|
+
withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
|
|
1353
|
+
return withWorkspaceSubjectRls(
|
|
1354
|
+
policyTx,
|
|
1355
|
+
workspaceId,
|
|
1356
|
+
scope.subjectId,
|
|
1357
|
+
async (actorTx) => {
|
|
1358
|
+
await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
|
|
1359
|
+
const replay = await getConnectBeginReplay(actorTx, scope, {
|
|
1360
|
+
idempotencyKey: input.idempotencyKey,
|
|
1361
|
+
requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
|
|
1362
|
+
returnUrl: input.returnUrl,
|
|
1363
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
1364
|
+
});
|
|
1365
|
+
if (replay) return replay;
|
|
1366
|
+
assertOrganizationIntegrationAllowed(
|
|
1367
|
+
policy,
|
|
1368
|
+
integrationKeyForConnectProvider(input.providerId),
|
|
1369
|
+
);
|
|
1370
|
+
return null;
|
|
1371
|
+
},
|
|
1372
|
+
);
|
|
1373
|
+
});
|
|
1374
|
+
const replay = await admit();
|
|
1375
|
+
if (replay) return replay;
|
|
1376
|
+
const beginConnectAttempt = async (
|
|
1377
|
+
_db: Parameters<typeof persistConnectAttempt>[0],
|
|
1378
|
+
_scope: Parameters<typeof persistConnectAttempt>[1],
|
|
1379
|
+
attemptInput: Parameters<typeof persistConnectAttempt>[2],
|
|
1380
|
+
) =>
|
|
1381
|
+
withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
|
|
1382
|
+
return withWorkspaceSubjectRls(
|
|
1383
|
+
policyTx,
|
|
1384
|
+
workspaceId,
|
|
1385
|
+
scope.subjectId,
|
|
1386
|
+
async (actorTx) => {
|
|
1387
|
+
await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
|
|
1388
|
+
return persistConnectAttempt(actorTx, scope, {
|
|
1389
|
+
...attemptInput,
|
|
1390
|
+
authorizeAcquisition: async () => {
|
|
1391
|
+
assertOrganizationIntegrationAllowed(
|
|
1392
|
+
policy,
|
|
1393
|
+
integrationKeyForConnectProvider(input.providerId),
|
|
1394
|
+
);
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
},
|
|
1398
|
+
);
|
|
1399
|
+
});
|
|
1307
1400
|
const id = randomUUID();
|
|
1308
1401
|
if (input.providerId === "mcp-install") {
|
|
1309
1402
|
if (
|
|
@@ -1552,6 +1645,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1552
1645
|
[
|
|
1553
1646
|
"mcp-oauth",
|
|
1554
1647
|
"slack-personal",
|
|
1648
|
+
"gmail",
|
|
1555
1649
|
"mcp-bearer",
|
|
1556
1650
|
"mcp-headers",
|
|
1557
1651
|
"fiken-token",
|
|
@@ -1567,18 +1661,27 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1567
1661
|
message: "MCP credential setup does not install an API integration instance",
|
|
1568
1662
|
});
|
|
1569
1663
|
if (
|
|
1570
|
-
[
|
|
1571
|
-
|
|
1572
|
-
|
|
1664
|
+
[
|
|
1665
|
+
"mcp-oauth",
|
|
1666
|
+
"slack-personal",
|
|
1667
|
+
"gmail",
|
|
1668
|
+
"mcp-bearer",
|
|
1669
|
+
"mcp-headers",
|
|
1670
|
+
"fiken-token",
|
|
1671
|
+
].includes(input.providerId)
|
|
1573
1672
|
) {
|
|
1574
1673
|
if (input.providerId === "slack-personal" && input.ownership !== "personal")
|
|
1575
1674
|
throw new HTTPException(422, {
|
|
1576
1675
|
message: "Hosted Slack MCP requires personal ownership",
|
|
1577
1676
|
});
|
|
1677
|
+
if (input.providerId === "gmail" && input.ownership !== "personal")
|
|
1678
|
+
throw new HTTPException(422, {
|
|
1679
|
+
message: "Gmail requires personal ownership; each user connects their own account",
|
|
1680
|
+
});
|
|
1578
1681
|
if (input.providerId === "fiken-token" && input.ownership !== "workspace")
|
|
1579
1682
|
throw new HTTPException(422, { message: "Fiken is workspace-owned" });
|
|
1580
1683
|
requireEnvironmentEncryption(deps.settings);
|
|
1581
|
-
if (["mcp-oauth", "slack-personal"].includes(input.providerId))
|
|
1684
|
+
if (["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId))
|
|
1582
1685
|
requireIntegrationsStateSecret(deps.settings);
|
|
1583
1686
|
const existing = input.reconnectAccountId
|
|
1584
1687
|
? await getConnectionMetadata(
|
|
@@ -1593,11 +1696,13 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1593
1696
|
(!existing ||
|
|
1594
1697
|
(input.providerId === "fiken-token"
|
|
1595
1698
|
? !isFikenConnection(existing)
|
|
1596
|
-
: ["mcp-oauth", "slack-personal"].includes(input.providerId)
|
|
1699
|
+
: ["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId)
|
|
1597
1700
|
? existing.kind !== "oauth2" ||
|
|
1598
1701
|
!existing.metadata.oauthDiscovery ||
|
|
1599
1702
|
(input.providerId === "slack-personal" &&
|
|
1600
|
-
existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL)
|
|
1703
|
+
existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL) ||
|
|
1704
|
+
(input.providerId === "gmail" &&
|
|
1705
|
+
existing.metadata.mcpUrl !== OFFICIAL_GMAIL_MCP_URL)
|
|
1601
1706
|
: existing.kind !== "api_key" ||
|
|
1602
1707
|
existing.metadata.connectAdapter !== input.providerId) ||
|
|
1603
1708
|
(existing.subjectId === null ? "workspace" : "personal") !== input.ownership)
|
|
@@ -1634,51 +1739,50 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1634
1739
|
: {}),
|
|
1635
1740
|
nextAction: {
|
|
1636
1741
|
type: "credentials",
|
|
1637
|
-
fields:
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1742
|
+
fields: ["slack-personal", "gmail"].includes(input.providerId)
|
|
1743
|
+
? []
|
|
1744
|
+
: input.providerId === "fiken-token"
|
|
1745
|
+
? [
|
|
1746
|
+
{
|
|
1747
|
+
name: "apiToken",
|
|
1748
|
+
label: "Fiken API token",
|
|
1749
|
+
required: true,
|
|
1750
|
+
secret: true,
|
|
1751
|
+
},
|
|
1752
|
+
{
|
|
1753
|
+
name: "defaultCompanySlug",
|
|
1754
|
+
label: "Default company slug (optional)",
|
|
1755
|
+
required: false,
|
|
1756
|
+
secret: false,
|
|
1757
|
+
},
|
|
1758
|
+
]
|
|
1759
|
+
: [
|
|
1760
|
+
{
|
|
1761
|
+
name: "mcpUrl",
|
|
1762
|
+
label: "MCP server URL",
|
|
1763
|
+
required: true,
|
|
1764
|
+
secret: false,
|
|
1765
|
+
},
|
|
1766
|
+
...(input.providerId === "mcp-bearer"
|
|
1767
|
+
? [
|
|
1768
|
+
{
|
|
1769
|
+
name: "token",
|
|
1770
|
+
label: "Bearer credential",
|
|
1771
|
+
required: true,
|
|
1772
|
+
secret: true,
|
|
1773
|
+
},
|
|
1774
|
+
]
|
|
1775
|
+
: input.providerId === "mcp-headers"
|
|
1663
1776
|
? [
|
|
1664
1777
|
{
|
|
1665
|
-
name: "
|
|
1666
|
-
label: "
|
|
1778
|
+
name: "headers",
|
|
1779
|
+
label: "Credential headers (JSON)",
|
|
1667
1780
|
required: true,
|
|
1668
1781
|
secret: true,
|
|
1669
1782
|
},
|
|
1670
1783
|
]
|
|
1671
|
-
:
|
|
1672
|
-
|
|
1673
|
-
{
|
|
1674
|
-
name: "headers",
|
|
1675
|
-
label: "Credential headers (JSON)",
|
|
1676
|
-
required: true,
|
|
1677
|
-
secret: true,
|
|
1678
|
-
},
|
|
1679
|
-
]
|
|
1680
|
-
: []),
|
|
1681
|
-
],
|
|
1784
|
+
: []),
|
|
1785
|
+
],
|
|
1682
1786
|
},
|
|
1683
1787
|
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
1684
1788
|
},
|
|
@@ -1838,7 +1942,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1838
1942
|
expiresAt: started.expiresAt,
|
|
1839
1943
|
},
|
|
1840
1944
|
});
|
|
1841
|
-
}),
|
|
1945
|
+
})(),
|
|
1842
1946
|
);
|
|
1843
1947
|
});
|
|
1844
1948
|
app.get("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
|