@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.
Files changed (39) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-7IAWEYBH.js → chunk-XZGTKYOA.js} +3626 -2525
  3. package/dist/{chunk-7IAWEYBH.js.map → chunk-XZGTKYOA.js.map} +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/integrations/fiken.d.ts +3 -1
  6. package/dist/integrations/github-lens-connect.d.ts +2 -2
  7. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  8. package/dist/integrations/oauth-client.d.ts +118 -1
  9. package/dist/integrations/slack-interactions.d.ts +1 -1
  10. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  11. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  12. package/dist/routes/api-integrations.d.ts +1 -0
  13. package/dist/routes/organization-integration-policy.d.ts +3 -0
  14. package/package.json +19 -19
  15. package/src/app.ts +7 -2
  16. package/src/integrations/atlassian.ts +135 -65
  17. package/src/integrations/fiken.ts +57 -35
  18. package/src/integrations/github-app-connect.ts +34 -15
  19. package/src/integrations/google-drive.ts +295 -124
  20. package/src/integrations/integration-commit-authority.ts +69 -0
  21. package/src/integrations/oauth-client.ts +220 -65
  22. package/src/integrations/personal-github.ts +55 -31
  23. package/src/integrations/provider-oauth.ts +54 -31
  24. package/src/integrations/social-oauth.ts +83 -56
  25. package/src/mcp/first-party-tool-permissions.ts +1 -0
  26. package/src/mcp/knowledge-entries.ts +23 -6
  27. package/src/routes/api-integrations.ts +116 -4
  28. package/src/routes/billing.ts +52 -0
  29. package/src/routes/capabilities.ts +51 -1
  30. package/src/routes/connect.ts +161 -57
  31. package/src/routes/connections.ts +430 -305
  32. package/src/routes/github.ts +48 -19
  33. package/src/routes/integration-facets.ts +66 -25
  34. package/src/routes/knowledge.ts +48 -1
  35. package/src/routes/organization-integration-policy.ts +113 -0
  36. package/src/routes/plugins.ts +12 -2
  37. package/src/routes/pr-review-github.ts +60 -30
  38. package/src/routes/pr-review.ts +69 -45
  39. package/src/routes/social.ts +24 -13
@@ -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 { requireAccessGrant } from "@opengeni/core";
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");
@@ -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
- return c.json([
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
- : "mcp-oauth",
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
- : z
1017
- .object({ mcpUrl: z.string().url().max(4096) })
1018
- .strict()
1019
- .parse(action.values);
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 withWorkspaceSubjectRls(deps.db, workspaceId, scope.subjectId, async (tx) => {
1306
- await requireConnectOwnerAuthority(tx, scope, setupPermission);
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
- ["mcp-oauth", "slack-personal", "mcp-bearer", "mcp-headers", "fiken-token"].includes(
1571
- input.providerId,
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
- 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"
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: "token",
1666
- label: "Bearer credential",
1778
+ name: "headers",
1779
+ label: "Credential headers (JSON)",
1667
1780
  required: true,
1668
1781
  secret: true,
1669
1782
  },
1670
1783
  ]
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
- ],
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) => {