@opengeni/api-router 0.23.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/browser-controller-authority.d.ts +43 -0
  4. package/dist/browser-state-authority.d.ts +27 -0
  5. package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
  6. package/dist/chunk-JIKNR5YL.js.map +1 -0
  7. package/dist/codemode.d.ts +23 -0
  8. package/dist/editable-artifact-live-hints.d.ts +11 -0
  9. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  10. package/dist/editable-artifact-office-import.d.ts +22 -0
  11. package/dist/editable-artifact-production.d.ts +29 -0
  12. package/dist/editable-artifact-websocket.d.ts +49 -0
  13. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  14. package/dist/github-browser-flow.d.ts +6 -0
  15. package/dist/http/cors.d.ts +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1796 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/api-integrations.d.ts +24 -0
  20. package/dist/integrations/atlassian.d.ts +176 -0
  21. package/dist/integrations/github-skill-source.d.ts +5 -0
  22. package/dist/integrations/google-drive.d.ts +85 -0
  23. package/dist/integrations/oauth-client.d.ts +22 -1
  24. package/dist/integrations/provider-oauth.d.ts +19 -0
  25. package/dist/integrations/slack-bot.d.ts +4 -0
  26. package/dist/integrations/slack-interactions.d.ts +8 -2
  27. package/dist/integrations/social-api.d.ts +2 -1
  28. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  29. package/dist/mcp/editable-artifacts.d.ts +13 -0
  30. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  31. package/dist/mcp/server.d.ts +14 -3
  32. package/dist/memory-slack-delivery.d.ts +9 -0
  33. package/dist/routes/api-integrations.d.ts +8 -0
  34. package/dist/routes/browser-identities.d.ts +5 -0
  35. package/dist/routes/browser-sessions.d.ts +6 -0
  36. package/dist/routes/company-profile.d.ts +3 -0
  37. package/dist/routes/computer-sessions.d.ts +6 -0
  38. package/dist/routes/editable-artifacts.d.ts +44 -0
  39. package/dist/routes/integration-features.d.ts +3 -0
  40. package/dist/routes/memory-slack-publications.d.ts +6 -0
  41. package/dist/routes/plugins.d.ts +8 -0
  42. package/dist/routes/sessions.d.ts +17 -2
  43. package/dist/routes/skills.d.ts +6 -0
  44. package/dist/routes/video-generation.d.ts +3 -0
  45. package/dist/sandbox/channel-a.d.ts +55 -2
  46. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  47. package/dist/sandbox/viewer.d.ts +4 -2
  48. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  49. package/package.json +19 -14
  50. package/src/app.ts +274 -37
  51. package/src/browser-controller-authority.ts +137 -0
  52. package/src/browser-state-authority.ts +236 -0
  53. package/src/codemode.ts +186 -0
  54. package/src/editable-artifact-live-hints.ts +64 -0
  55. package/src/editable-artifact-native-kernel.ts +659 -0
  56. package/src/editable-artifact-office-import.ts +230 -0
  57. package/src/editable-artifact-production.ts +419 -0
  58. package/src/editable-artifact-websocket.ts +311 -0
  59. package/src/editable-artifact-workspace-files.ts +186 -0
  60. package/src/github-browser-flow.ts +35 -6
  61. package/src/http/auth.ts +2 -0
  62. package/src/http/cors.ts +3 -0
  63. package/src/index.ts +119 -20
  64. package/src/integrations/api-integrations.ts +350 -0
  65. package/src/integrations/atlassian.ts +1621 -0
  66. package/src/integrations/github-skill-source.ts +142 -0
  67. package/src/integrations/google-drive.ts +1000 -64
  68. package/src/integrations/oauth-client.ts +123 -33
  69. package/src/integrations/provider-oauth.ts +777 -0
  70. package/src/integrations/slack-bot.ts +31 -2
  71. package/src/integrations/slack-interactions.ts +487 -27
  72. package/src/integrations/social-api.ts +11 -0
  73. package/src/mcp/documents.ts +32 -1
  74. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  75. package/src/mcp/editable-artifacts.ts +448 -0
  76. package/src/mcp/scheduled-task-view.ts +34 -0
  77. package/src/mcp/server.ts +923 -145
  78. package/src/memory-slack-delivery.ts +209 -0
  79. package/src/routes/api-integrations.ts +407 -0
  80. package/src/routes/browser-identities.ts +136 -0
  81. package/src/routes/browser-sessions.ts +2543 -0
  82. package/src/routes/company-profile.ts +255 -0
  83. package/src/routes/computer-sessions.ts +1247 -0
  84. package/src/routes/connections.ts +284 -99
  85. package/src/routes/documents.ts +22 -3
  86. package/src/routes/editable-artifacts.ts +1159 -0
  87. package/src/routes/files.ts +106 -4
  88. package/src/routes/github.ts +18 -2
  89. package/src/routes/install.ts +7 -1
  90. package/src/routes/integration-features.ts +258 -0
  91. package/src/routes/memory-slack-publications.ts +216 -0
  92. package/src/routes/packs.ts +437 -7
  93. package/src/routes/plugins.ts +751 -0
  94. package/src/routes/rigs.ts +77 -20
  95. package/src/routes/scheduled-tasks.ts +67 -40
  96. package/src/routes/sessions.ts +421 -245
  97. package/src/routes/skills.ts +174 -0
  98. package/src/routes/video-generation.ts +132 -0
  99. package/src/routes/workspaces.ts +46 -24
  100. package/src/sandbox/channel-a.ts +723 -116
  101. package/src/sandbox/metrics-ingestion.ts +121 -3
  102. package/src/sandbox/rematerialize.ts +35 -47
  103. package/src/sandbox/viewer.ts +29 -9
  104. package/src/temporal-schedule-cleanup.ts +135 -0
  105. package/dist/chunk-T4T2PGU4.js.map +0 -1
  106. package/dist/mcp/toolspace.d.ts +0 -71
  107. package/src/mcp/toolspace.ts +0 -1190
@@ -45,8 +45,16 @@ import { canonicalProviderDomain } from "./provider-domain";
45
45
 
46
46
  export const oauthStateTtlMs = 10 * 60 * 1000;
47
47
  export const OFFICIAL_SLACK_MCP_URL = OPENGENI_PERSONAL_SLACK_MCP_URL;
48
+ export const OFFICIAL_GMAIL_MCP_URL = "https://gmailmcp.googleapis.com/mcp/v1";
49
+ export const OFFICIAL_GMAIL_MCP_SCOPES = [
50
+ "https://www.googleapis.com/auth/gmail.readonly",
51
+ "https://www.googleapis.com/auth/gmail.compose",
52
+ "https://www.googleapis.com/auth/gmail.modify",
53
+ ] as const;
48
54
  const SLACK_OAUTH_ORIGIN = "https://slack.com";
49
55
  const SLACK_MCP_ORIGIN = "https://mcp.slack.com";
56
+ const GOOGLE_OAUTH_ISSUER_ORIGIN = "https://accounts.google.com";
57
+ const GOOGLE_TOKEN_ORIGIN = "https://oauth2.googleapis.com";
50
58
  export { OAUTH_MAX_RESPONSE_BYTES } from "@opengeni/network";
51
59
 
52
60
  type OAuthClientDeps = {
@@ -120,6 +128,7 @@ type OAuthStatePayload = {
120
128
  issuer: string;
121
129
  clientRegistrationMethod: OAuthClientRegistration["method"];
122
130
  tokenEndpointAuthMethod: OAuthClientRegistration["tokenEndpointAuthMethod"];
131
+ resourceParameterSupported: boolean;
123
132
  encryptedClientSecret?: string;
124
133
  returnPath: string;
125
134
  connectionId?: string;
@@ -296,21 +305,15 @@ async function startMcpOAuthWithinDeadline(
296
305
  ): Promise<OAuthStartResponse> {
297
306
  const { db, settings } = deps;
298
307
  const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
308
+ const officialGmailResource = mcpUrl === OFFICIAL_GMAIL_MCP_URL;
299
309
  const officialSlackResource = mcpUrl === OFFICIAL_SLACK_MCP_URL;
300
310
  const providerDomain = officialSlackResource
301
311
  ? "slack.com"
302
312
  : canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
303
- const personalSlack = officialSlackResource || providerDomain === "slack.com";
304
- assertPersonalSlackOAuthStart(settings, context.payload, mcpUrl, personalSlack);
305
- if (personalSlack && context.payload.ownership === "workspace") {
306
- throw new HTTPException(422, {
307
- message:
308
- "Slack's hosted MCP connection is personal; use the OpenGeni Slack bot installation for workspace access",
309
- });
310
- }
311
- const requestedOwnership: ConnectionOwnership = personalSlack
312
- ? "personal"
313
- : (context.payload.ownership ?? "workspace");
313
+ const hostedSlackMcp = officialSlackResource || providerDomain === "slack.com";
314
+ assertHostedSlackMcpOAuthStart(settings, context.payload, mcpUrl, hostedSlackMcp);
315
+ const requestedOwnership: ConnectionOwnership = context.payload.ownership ?? "workspace";
316
+ assertOfficialGmailPersonalOwnership(mcpUrl, requestedOwnership);
314
317
  const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
315
318
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
316
319
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
@@ -321,7 +324,8 @@ async function startMcpOAuthWithinDeadline(
321
324
  subjectId: context.subjectId,
322
325
  providerDomain,
323
326
  mcpUrl,
324
- personalSlack,
327
+ hostedSlackMcp,
328
+ exactMcpBinding: hostedSlackMcp || officialGmailResource,
325
329
  connectionId: context.payload.connectionId,
326
330
  requestedOwnership: context.payload.ownership,
327
331
  newConnectionOwnership: requestedOwnership,
@@ -335,16 +339,29 @@ async function startMcpOAuthWithinDeadline(
335
339
  : requestedOwnership;
336
340
 
337
341
  const discovery = await discoverMcpOAuth(mcpUrl, settings, deadline);
338
- if (personalSlack && !isLocalTestEnvironment(settings.environment)) {
342
+ if (hostedSlackMcp && !isLocalTestEnvironment(settings.environment)) {
339
343
  assertSlackAuthorizationServer(discovery.as);
340
344
  }
345
+ const googleAuthorizationServer = hasGoogleAuthorizationServerIdentity(discovery.as);
346
+ if (officialGmailResource || googleAuthorizationServer) {
347
+ if (!officialGmailResource || providerDomain !== "gmailmcp.googleapis.com") {
348
+ throw new HTTPException(422, {
349
+ message: `Google OAuth is allowed only for ${OFFICIAL_GMAIL_MCP_URL}`,
350
+ });
351
+ }
352
+ assertGoogleAuthorizationServer(discovery.as);
353
+ }
354
+ const resourceParameterSupported = !officialGmailResource;
341
355
  const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
342
356
  const verifier = randomPkceVerifier();
343
- const authorizeScopes = chooseAuthorizeScopes(
344
- context.payload.requestedScopes,
345
- discovery.challenge.scope,
346
- discovery.prm.scopesSupported,
347
- );
357
+ // The Gmail PRM advertises broader grants, including full-mail access. The
358
+ // reviewed connector never lets a caller widen the capability contract.
359
+ const authorizeScopes = chooseMcpAuthorizeScopes({
360
+ mcpUrl,
361
+ requested: context.payload.requestedScopes,
362
+ challenged: discovery.challenge.scope,
363
+ supported: discovery.prm.scopesSupported,
364
+ });
348
365
  const client = await deadline.run("client_registration", (signal) =>
349
366
  registerOAuthClient(
350
367
  db,
@@ -366,7 +383,9 @@ async function startMcpOAuthWithinDeadline(
366
383
  providerDomain,
367
384
  mcpUrl,
368
385
  resource,
369
- requestedScopes: uniqueStrings(context.payload.requestedScopes ?? []),
386
+ requestedScopes: officialGmailResource
387
+ ? [...OFFICIAL_GMAIL_MCP_SCOPES]
388
+ : uniqueStrings(context.payload.requestedScopes ?? []),
370
389
  authorizeScopes,
371
390
  encryptedPkceVerifier: encryptEnvironmentValue(key, verifier),
372
391
  clientId: client.clientId,
@@ -375,6 +394,7 @@ async function startMcpOAuthWithinDeadline(
375
394
  issuer: client.issuer,
376
395
  clientRegistrationMethod: client.method,
377
396
  tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
397
+ resourceParameterSupported,
378
398
  ...(client.method === "manual" && client.clientSecret
379
399
  ? {
380
400
  encryptedClientSecret: encryptEnvironmentValue(key, client.clientSecret),
@@ -392,6 +412,7 @@ async function startMcpOAuthWithinDeadline(
392
412
  resource,
393
413
  verifier,
394
414
  scopes: authorizeScopes,
415
+ resourceParameterSupported,
395
416
  });
396
417
  return OAuthStartResponse.parse({
397
418
  state,
@@ -400,6 +421,18 @@ async function startMcpOAuthWithinDeadline(
400
421
  });
401
422
  }
402
423
 
424
+ export function assertOfficialGmailPersonalOwnership(
425
+ mcpUrl: string,
426
+ ownership: ConnectionOwnership,
427
+ ): void {
428
+ if (mcpUrl === OFFICIAL_GMAIL_MCP_URL && ownership !== "personal") {
429
+ throw new HTTPException(422, {
430
+ message:
431
+ "Gmail connections are personal only; each workspace member must connect their own mailbox",
432
+ });
433
+ }
434
+ }
435
+
403
436
  export async function completeMcpOAuthCallback(
404
437
  deps: OAuthClientDeps,
405
438
  input: {
@@ -445,6 +478,9 @@ async function completeMcpOAuthCallbackWithinDeadline(
445
478
  }
446
479
  try {
447
480
  state = readOAuthState(input.state, settings);
481
+ // Fence state minted by an older deployment too: a rolling update must not
482
+ // let a still-valid workspace-owned Gmail callback persist shared authority.
483
+ assertOfficialGmailPersonalOwnership(state.mcpUrl, state.ownership);
448
484
  if (!input.code) {
449
485
  return {
450
486
  redirectTo: callbackReturnPath(state.returnPath, "error", {
@@ -505,6 +541,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
505
541
  resource: state.resource,
506
542
  tokenEndpoint: state.tokenEndpoint,
507
543
  client,
544
+ resourceParameterSupported: state.resourceParameterSupported,
508
545
  signal,
509
546
  }),
510
547
  );
@@ -619,13 +656,13 @@ async function requireOAuthCallbackGrant(db: Database, state: OAuthStatePayload)
619
656
  }
620
657
  }
621
658
 
622
- function assertPersonalSlackOAuthStart(
659
+ function assertHostedSlackMcpOAuthStart(
623
660
  settings: Settings,
624
661
  payload: OAuthStartRequest,
625
662
  mcpUrl: string,
626
- personalSlack: boolean,
663
+ hostedSlackMcp: boolean,
627
664
  ): void {
628
- if (!personalSlack) return;
665
+ if (!hostedSlackMcp) return;
629
666
  if (payload.oauthClient) {
630
667
  throw new HTTPException(422, {
631
668
  message: "Slack OAuth client credentials are deployment-managed",
@@ -638,13 +675,12 @@ function assertPersonalSlackOAuthStart(
638
675
  }
639
676
  if (!isLocalTestEnvironment(settings.environment) && mcpUrl !== OFFICIAL_SLACK_MCP_URL) {
640
677
  throw new HTTPException(422, {
641
- message: `personal Slack OAuth must use ${OFFICIAL_SLACK_MCP_URL}`,
678
+ message: `Slack MCP OAuth must use ${OFFICIAL_SLACK_MCP_URL}`,
642
679
  });
643
680
  }
644
681
  if (!settings.slackClientId?.trim() || !settings.slackClientSecret?.trim()) {
645
682
  throw new HTTPException(503, {
646
- message:
647
- "personal Slack OAuth requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET",
683
+ message: "Slack MCP OAuth requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET",
648
684
  });
649
685
  }
650
686
  }
@@ -664,6 +700,27 @@ export function assertSlackAuthorizationServer(as: AuthorizationServerMetadata):
664
700
  }
665
701
  }
666
702
 
703
+ function hasGoogleAuthorizationServerIdentity(as: AuthorizationServerMetadata): boolean {
704
+ return [as.issuer, as.authorizationServer].some(
705
+ (value) => new URL(value).origin === GOOGLE_OAUTH_ISSUER_ORIGIN,
706
+ );
707
+ }
708
+
709
+ export function assertGoogleAuthorizationServer(as: AuthorizationServerMetadata): void {
710
+ const issuerOrigins = [as.issuer, as.authorizationServer].map((value) => new URL(value).origin);
711
+ const authorizationOrigin = new URL(as.authorizationEndpoint).origin;
712
+ const tokenOrigin = new URL(as.tokenEndpoint).origin;
713
+ if (
714
+ issuerOrigins.some((origin) => origin !== GOOGLE_OAUTH_ISSUER_ORIGIN) ||
715
+ authorizationOrigin !== GOOGLE_OAUTH_ISSUER_ORIGIN ||
716
+ tokenOrigin !== GOOGLE_TOKEN_ORIGIN
717
+ ) {
718
+ throw new HTTPException(422, {
719
+ message: "Gmail MCP authorization metadata did not remain bound to Google",
720
+ });
721
+ }
722
+ }
723
+
667
724
  async function discoverMcpOAuth(
668
725
  resource: string,
669
726
  settings: Settings,
@@ -1153,7 +1210,8 @@ async function existingOAuthConnectionForStart(
1153
1210
  subjectId: string;
1154
1211
  providerDomain: string;
1155
1212
  mcpUrl: string;
1156
- personalSlack: boolean;
1213
+ hostedSlackMcp: boolean;
1214
+ exactMcpBinding: boolean;
1157
1215
  connectionId?: string | undefined;
1158
1216
  requestedOwnership?: ConnectionOwnership | undefined;
1159
1217
  newConnectionOwnership: ConnectionOwnership;
@@ -1176,7 +1234,7 @@ async function existingOAuthConnectionForStart(
1176
1234
  });
1177
1235
  }
1178
1236
  return connection.providerDomain === input.providerDomain &&
1179
- (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl)
1237
+ (!input.exactMcpBinding || connection.metadata.mcpUrl === input.mcpUrl)
1180
1238
  ? connection
1181
1239
  : null;
1182
1240
  }
@@ -1187,9 +1245,9 @@ async function existingOAuthConnectionForStart(
1187
1245
  connection.subjectId === ownerSubjectId &&
1188
1246
  connection.kind === "oauth2" &&
1189
1247
  connection.providerDomain === input.providerDomain &&
1190
- (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl),
1248
+ (!input.exactMcpBinding || connection.metadata.mcpUrl === input.mcpUrl),
1191
1249
  );
1192
- if (input.personalSlack) {
1250
+ if (input.hostedSlackMcp && input.newConnectionOwnership === "personal") {
1193
1251
  return selectCanonicalPersonalSlackConnection(matching);
1194
1252
  }
1195
1253
  return matching.find((connection) => connection.status === "active") ?? null;
@@ -1204,7 +1262,7 @@ function ownershipForConnection(
1204
1262
  throw new HTTPException(404, { message: "connection not found" });
1205
1263
  }
1206
1264
 
1207
- function buildAuthorizationUrl(input: {
1265
+ export function buildAuthorizationUrl(input: {
1208
1266
  endpoint: string;
1209
1267
  settings: Settings;
1210
1268
  clientId: string;
@@ -1213,6 +1271,7 @@ function buildAuthorizationUrl(input: {
1213
1271
  resource: string;
1214
1272
  verifier: string;
1215
1273
  scopes: string[];
1274
+ resourceParameterSupported: boolean;
1216
1275
  }): string {
1217
1276
  const endpoint = oauthEndpointUrl(input.endpoint, input.settings, "OAuth authorization endpoint");
1218
1277
  const url = new URL(endpoint);
@@ -1220,7 +1279,16 @@ function buildAuthorizationUrl(input: {
1220
1279
  url.searchParams.set("client_id", input.clientId);
1221
1280
  url.searchParams.set("redirect_uri", input.redirectUri);
1222
1281
  url.searchParams.set("state", input.state);
1223
- url.searchParams.set("resource", input.resource);
1282
+ if (input.resourceParameterSupported) {
1283
+ url.searchParams.set("resource", input.resource);
1284
+ } else {
1285
+ // Google's OAuth endpoints do not implement RFC 8707's `resource`
1286
+ // parameter. Explicit offline consent is required to obtain the refresh
1287
+ // token used by the durable connection broker.
1288
+ url.searchParams.set("access_type", "offline");
1289
+ url.searchParams.set("include_granted_scopes", "true");
1290
+ url.searchParams.set("prompt", "consent");
1291
+ }
1224
1292
  url.searchParams.set("code_challenge_method", "S256");
1225
1293
  url.searchParams.set("code_challenge", pkceChallenge(input.verifier));
1226
1294
  if (input.scopes.length > 0) {
@@ -1277,6 +1345,9 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1277
1345
  ),
1278
1346
  clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
1279
1347
  tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.tokenEndpointAuthMethod), false),
1348
+ // States minted before provider-specific compatibility was introduced used
1349
+ // the RFC 8707 resource parameter.
1350
+ resourceParameterSupported: payload.resourceParameterSupported !== false,
1280
1351
  ...(stringValue(payload.encryptedClientSecret)
1281
1352
  ? { encryptedClientSecret: stringValue(payload.encryptedClientSecret)! }
1282
1353
  : {}),
@@ -1379,6 +1450,7 @@ async function exchangeAuthorizationCode(
1379
1450
  verifier: string;
1380
1451
  redirectUri: string;
1381
1452
  resource: string;
1453
+ resourceParameterSupported: boolean;
1382
1454
  tokenEndpoint: string;
1383
1455
  client: OAuthClientRegistration;
1384
1456
  signal: AbortSignal;
@@ -1389,7 +1461,9 @@ async function exchangeAuthorizationCode(
1389
1461
  body.set("code", input.code);
1390
1462
  body.set("redirect_uri", input.redirectUri);
1391
1463
  body.set("code_verifier", input.verifier);
1392
- body.set("resource", input.resource);
1464
+ if (input.resourceParameterSupported) {
1465
+ body.set("resource", input.resource);
1466
+ }
1393
1467
  const headers: Record<string, string> = {
1394
1468
  "content-type": "application/x-www-form-urlencoded",
1395
1469
  accept: "application/json",
@@ -1512,7 +1586,11 @@ function oauthCallbackFailureReason(stage: OAuthCallbackStage, error: unknown):
1512
1586
  function isDatabaseStatementTimeout(error: unknown): boolean {
1513
1587
  let current = error;
1514
1588
  for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
1515
- const candidate = current as { code?: unknown; message?: unknown; cause?: unknown };
1589
+ const candidate = current as {
1590
+ code?: unknown;
1591
+ message?: unknown;
1592
+ cause?: unknown;
1593
+ };
1516
1594
  if (
1517
1595
  candidate.code === "57014" ||
1518
1596
  (typeof candidate.message === "string" &&
@@ -1727,6 +1805,7 @@ function credentialBundle(
1727
1805
  token_type: token.tokenType,
1728
1806
  ...(token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {}),
1729
1807
  resource: state.resource,
1808
+ resource_parameter_supported: state.resourceParameterSupported,
1730
1809
  mcp_url: state.mcpUrl,
1731
1810
  ...(token.scopeText
1732
1811
  ? { scope: token.scopeText }
@@ -1976,6 +2055,17 @@ function chooseAuthorizeScopes(
1976
2055
  return uniqueStrings(supported);
1977
2056
  }
1978
2057
 
2058
+ export function chooseMcpAuthorizeScopes(input: {
2059
+ mcpUrl: string;
2060
+ requested: string[] | undefined;
2061
+ challenged: string[] | undefined;
2062
+ supported: string[];
2063
+ }): string[] {
2064
+ return input.mcpUrl === OFFICIAL_GMAIL_MCP_URL
2065
+ ? [...OFFICIAL_GMAIL_MCP_SCOPES]
2066
+ : chooseAuthorizeScopes(input.requested, input.challenged, input.supported);
2067
+ }
2068
+
1979
2069
  function grantedScopes(scopeText: string | undefined, fallback: string[]): string[] {
1980
2070
  if (scopeText) {
1981
2071
  return uniqueStrings(scopeText.split(/\s+/).filter(Boolean));