@opengeni/api-router 2.13.0-canary.2 → 3.0.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,7 @@ import {
75
75
  import { resolveForRoute, validatedIntegrationInstallInput } from "./api-integrations";
76
76
  import { executeConnectOperation } from "@opengeni/core";
77
77
  import { startMcpOAuth, requireIntegrationsStateSecret } from "../integrations/oauth-client";
78
+ import { OFFICIAL_GMAIL_MCP_URL } from "../integrations/oauth-profiles";
78
79
  import { z } from "zod";
79
80
  import {
80
81
  WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
@@ -227,6 +228,19 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
227
228
  ownership: ["workspace"],
228
229
  setup: ["oauth"],
229
230
  }),
231
+ ConnectProvider.parse({
232
+ id: "gmail",
233
+ label: "Gmail",
234
+ family: "google",
235
+ readiness:
236
+ !external || !canWrite || !personal
237
+ ? "unsupported"
238
+ : mcpConfigured
239
+ ? "available"
240
+ : "needs_configuration",
241
+ ownership: personal ? ["personal"] : [],
242
+ setup: ["oauth"],
243
+ }),
230
244
  ConnectProvider.parse({
231
245
  id: "slack-personal",
232
246
  label: "My Slack account",
@@ -557,7 +571,9 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
557
571
  providerId:
558
572
  connection.metadata.mcpUrl === OPENGENI_PERSONAL_SLACK_MCP_URL
559
573
  ? "slack-personal"
560
- : "mcp-oauth",
574
+ : connection.metadata.mcpUrl === OFFICIAL_GMAIL_MCP_URL
575
+ ? "gmail"
576
+ : "mcp-oauth",
561
577
  label: connection.providerDomain,
562
578
  ownership: connection.subjectId === null ? "workspace" : "personal",
563
579
  status:
@@ -1007,16 +1023,18 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
1007
1023
  }),
1008
1024
  );
1009
1025
  }
1010
- if (["mcp-oauth", "slack-personal"].includes(before.providerId)) {
1026
+ if (["mcp-oauth", "slack-personal", "gmail"].includes(before.providerId)) {
1011
1027
  if (action.type !== "credentials")
1012
1028
  throw new HTTPException(422, { message: "MCP OAuth requires a server URL" });
1013
1029
  const values =
1014
1030
  before.providerId === "slack-personal"
1015
1031
  ? { mcpUrl: OPENGENI_PERSONAL_SLACK_MCP_URL }
1016
- : z
1017
- .object({ mcpUrl: z.string().url().max(4096) })
1018
- .strict()
1019
- .parse(action.values);
1032
+ : before.providerId === "gmail"
1033
+ ? { mcpUrl: OFFICIAL_GMAIL_MCP_URL }
1034
+ : z
1035
+ .object({ mcpUrl: z.string().url().max(4096) })
1036
+ .strict()
1037
+ .parse(action.values);
1020
1038
  return c.json(
1021
1039
  await executeConnectOperation({
1022
1040
  db: deps.db,
@@ -1552,6 +1570,7 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
1552
1570
  [
1553
1571
  "mcp-oauth",
1554
1572
  "slack-personal",
1573
+ "gmail",
1555
1574
  "mcp-bearer",
1556
1575
  "mcp-headers",
1557
1576
  "fiken-token",
@@ -1567,18 +1586,27 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
1567
1586
  message: "MCP credential setup does not install an API integration instance",
1568
1587
  });
1569
1588
  if (
1570
- ["mcp-oauth", "slack-personal", "mcp-bearer", "mcp-headers", "fiken-token"].includes(
1571
- input.providerId,
1572
- )
1589
+ [
1590
+ "mcp-oauth",
1591
+ "slack-personal",
1592
+ "gmail",
1593
+ "mcp-bearer",
1594
+ "mcp-headers",
1595
+ "fiken-token",
1596
+ ].includes(input.providerId)
1573
1597
  ) {
1574
1598
  if (input.providerId === "slack-personal" && input.ownership !== "personal")
1575
1599
  throw new HTTPException(422, {
1576
1600
  message: "Hosted Slack MCP requires personal ownership",
1577
1601
  });
1602
+ if (input.providerId === "gmail" && input.ownership !== "personal")
1603
+ throw new HTTPException(422, {
1604
+ message: "Gmail requires personal ownership; each user connects their own account",
1605
+ });
1578
1606
  if (input.providerId === "fiken-token" && input.ownership !== "workspace")
1579
1607
  throw new HTTPException(422, { message: "Fiken is workspace-owned" });
1580
1608
  requireEnvironmentEncryption(deps.settings);
1581
- if (["mcp-oauth", "slack-personal"].includes(input.providerId))
1609
+ if (["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId))
1582
1610
  requireIntegrationsStateSecret(deps.settings);
1583
1611
  const existing = input.reconnectAccountId
1584
1612
  ? await getConnectionMetadata(
@@ -1593,11 +1621,13 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
1593
1621
  (!existing ||
1594
1622
  (input.providerId === "fiken-token"
1595
1623
  ? !isFikenConnection(existing)
1596
- : ["mcp-oauth", "slack-personal"].includes(input.providerId)
1624
+ : ["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId)
1597
1625
  ? existing.kind !== "oauth2" ||
1598
1626
  !existing.metadata.oauthDiscovery ||
1599
1627
  (input.providerId === "slack-personal" &&
1600
- existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL)
1628
+ existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL) ||
1629
+ (input.providerId === "gmail" &&
1630
+ existing.metadata.mcpUrl !== OFFICIAL_GMAIL_MCP_URL)
1601
1631
  : existing.kind !== "api_key" ||
1602
1632
  existing.metadata.connectAdapter !== input.providerId) ||
1603
1633
  (existing.subjectId === null ? "workspace" : "personal") !== input.ownership)
@@ -1634,51 +1664,50 @@ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
1634
1664
  : {}),
1635
1665
  nextAction: {
1636
1666
  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"
1667
+ fields: ["slack-personal", "gmail"].includes(input.providerId)
1668
+ ? []
1669
+ : input.providerId === "fiken-token"
1670
+ ? [
1671
+ {
1672
+ name: "apiToken",
1673
+ label: "Fiken API token",
1674
+ required: true,
1675
+ secret: true,
1676
+ },
1677
+ {
1678
+ name: "defaultCompanySlug",
1679
+ label: "Default company slug (optional)",
1680
+ required: false,
1681
+ secret: false,
1682
+ },
1683
+ ]
1684
+ : [
1685
+ {
1686
+ name: "mcpUrl",
1687
+ label: "MCP server URL",
1688
+ required: true,
1689
+ secret: false,
1690
+ },
1691
+ ...(input.providerId === "mcp-bearer"
1692
+ ? [
1693
+ {
1694
+ name: "token",
1695
+ label: "Bearer credential",
1696
+ required: true,
1697
+ secret: true,
1698
+ },
1699
+ ]
1700
+ : input.providerId === "mcp-headers"
1663
1701
  ? [
1664
1702
  {
1665
- name: "token",
1666
- label: "Bearer credential",
1703
+ name: "headers",
1704
+ label: "Credential headers (JSON)",
1667
1705
  required: true,
1668
1706
  secret: true,
1669
1707
  },
1670
1708
  ]
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
- ],
1709
+ : []),
1710
+ ],
1682
1711
  },
1683
1712
  expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
1684
1713
  },
@@ -909,7 +909,7 @@ function normalizeArtifactIdList(value: readonly string[]): readonly string[] {
909
909
  return ids;
910
910
  }
911
911
 
912
- function isInvisibleListCandidate(error: unknown): boolean {
912
+ export function isInvisibleListCandidate(error: unknown): boolean {
913
913
  return (
914
914
  (error instanceof EditableArtifactDomainError &&
915
915
  (error.code === "forbidden" || error.code === "not_found")) ||
@@ -1050,7 +1050,7 @@ async function readBoundedJson(request: Request, maxBytes: number): Promise<unkn
1050
1050
  }
1051
1051
  }
1052
1052
 
1053
- function editableArtifactHttpError(error: unknown): unknown {
1053
+ export function editableArtifactHttpError(error: unknown): unknown {
1054
1054
  if (error instanceof EditableArtifactDurableExportError) {
1055
1055
  switch (error.code) {
1056
1056
  case "invalid_request":
@@ -22,6 +22,7 @@ import {
22
22
  prepareGeneratedWorkspaceFile,
23
23
  getSessionAuthorityProjection,
24
24
  requireWorkspace,
25
+ recordSandboxFilePublication,
25
26
  withSessionRlsActorContext,
26
27
  } from "@opengeni/db";
27
28
  import { retryWhileMissing, type ObjectHead, type ObjectStorage } from "@opengeni/storage";
@@ -225,6 +226,12 @@ async function publishSandboxFileArtifactInScope(
225
226
  if (!artifact) {
226
227
  throw new HTTPException(502, { message: "published sandbox artifact is not ready" });
227
228
  }
229
+ await recordSandboxFilePublication(deps.db, {
230
+ accountId: input.grant.accountId,
231
+ workspaceId: input.grant.workspaceId,
232
+ fileId: file.id,
233
+ sourceSessionId: input.session.id,
234
+ });
228
235
  return SandboxFileArtifactReceipt.parse({
229
236
  type: "sandbox_file",
230
237
  sandboxPath: `/workspace/${path}`,
@@ -300,7 +307,7 @@ export function sandboxFileContentType(filename: string): string {
300
307
  );
301
308
  }
302
309
 
303
- function sandboxArtifactIdentity(input: {
310
+ export function sandboxArtifactIdentity(input: {
304
311
  workspaceId: string;
305
312
  sessionId: string;
306
313
  path: string;
@@ -330,7 +337,7 @@ function uuidFromDigest(digest: string, startByte: number): string {
330
337
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
331
338
  }
332
339
 
333
- function assertSandboxArtifactFile(
340
+ export function assertSandboxArtifactFile(
334
341
  file: NonNullable<Awaited<ReturnType<typeof getFile>>>,
335
342
  expected: {
336
343
  filename: string;