@opengeni/api-router 0.22.2 → 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.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
|
@@ -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;
|
|
@@ -282,8 +291,7 @@ export async function startMcpOAuth(
|
|
|
282
291
|
error instanceof OAuthStartStageError
|
|
283
292
|
? error
|
|
284
293
|
: new OAuthStartStageError("connection_lookup", oauthStartFailureReason(error), error);
|
|
285
|
-
|
|
286
|
-
logOAuthStartFailure(deps.observability, staged, providerDomain);
|
|
294
|
+
logOAuthStartFailure(deps.observability, staged);
|
|
287
295
|
throw oauthStartApiError(staged);
|
|
288
296
|
} finally {
|
|
289
297
|
deadline.dispose();
|
|
@@ -297,21 +305,15 @@ async function startMcpOAuthWithinDeadline(
|
|
|
297
305
|
): Promise<OAuthStartResponse> {
|
|
298
306
|
const { db, settings } = deps;
|
|
299
307
|
const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
|
|
308
|
+
const officialGmailResource = mcpUrl === OFFICIAL_GMAIL_MCP_URL;
|
|
300
309
|
const officialSlackResource = mcpUrl === OFFICIAL_SLACK_MCP_URL;
|
|
301
310
|
const providerDomain = officialSlackResource
|
|
302
311
|
? "slack.com"
|
|
303
312
|
: canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
message:
|
|
309
|
-
"Slack's hosted MCP connection is personal; use the OpenGeni Slack bot installation for workspace access",
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
const requestedOwnership: ConnectionOwnership = personalSlack
|
|
313
|
-
? "personal"
|
|
314
|
-
: (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);
|
|
315
317
|
const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
|
|
316
318
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
|
|
317
319
|
const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
|
|
@@ -322,7 +324,8 @@ async function startMcpOAuthWithinDeadline(
|
|
|
322
324
|
subjectId: context.subjectId,
|
|
323
325
|
providerDomain,
|
|
324
326
|
mcpUrl,
|
|
325
|
-
|
|
327
|
+
hostedSlackMcp,
|
|
328
|
+
exactMcpBinding: hostedSlackMcp || officialGmailResource,
|
|
326
329
|
connectionId: context.payload.connectionId,
|
|
327
330
|
requestedOwnership: context.payload.ownership,
|
|
328
331
|
newConnectionOwnership: requestedOwnership,
|
|
@@ -336,16 +339,29 @@ async function startMcpOAuthWithinDeadline(
|
|
|
336
339
|
: requestedOwnership;
|
|
337
340
|
|
|
338
341
|
const discovery = await discoverMcpOAuth(mcpUrl, settings, deadline);
|
|
339
|
-
if (
|
|
342
|
+
if (hostedSlackMcp && !isLocalTestEnvironment(settings.environment)) {
|
|
340
343
|
assertSlackAuthorizationServer(discovery.as);
|
|
341
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;
|
|
342
355
|
const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
|
|
343
356
|
const verifier = randomPkceVerifier();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
});
|
|
349
365
|
const client = await deadline.run("client_registration", (signal) =>
|
|
350
366
|
registerOAuthClient(
|
|
351
367
|
db,
|
|
@@ -367,7 +383,9 @@ async function startMcpOAuthWithinDeadline(
|
|
|
367
383
|
providerDomain,
|
|
368
384
|
mcpUrl,
|
|
369
385
|
resource,
|
|
370
|
-
requestedScopes:
|
|
386
|
+
requestedScopes: officialGmailResource
|
|
387
|
+
? [...OFFICIAL_GMAIL_MCP_SCOPES]
|
|
388
|
+
: uniqueStrings(context.payload.requestedScopes ?? []),
|
|
371
389
|
authorizeScopes,
|
|
372
390
|
encryptedPkceVerifier: encryptEnvironmentValue(key, verifier),
|
|
373
391
|
clientId: client.clientId,
|
|
@@ -376,6 +394,7 @@ async function startMcpOAuthWithinDeadline(
|
|
|
376
394
|
issuer: client.issuer,
|
|
377
395
|
clientRegistrationMethod: client.method,
|
|
378
396
|
tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
|
|
397
|
+
resourceParameterSupported,
|
|
379
398
|
...(client.method === "manual" && client.clientSecret
|
|
380
399
|
? {
|
|
381
400
|
encryptedClientSecret: encryptEnvironmentValue(key, client.clientSecret),
|
|
@@ -393,6 +412,7 @@ async function startMcpOAuthWithinDeadline(
|
|
|
393
412
|
resource,
|
|
394
413
|
verifier,
|
|
395
414
|
scopes: authorizeScopes,
|
|
415
|
+
resourceParameterSupported,
|
|
396
416
|
});
|
|
397
417
|
return OAuthStartResponse.parse({
|
|
398
418
|
state,
|
|
@@ -401,6 +421,18 @@ async function startMcpOAuthWithinDeadline(
|
|
|
401
421
|
});
|
|
402
422
|
}
|
|
403
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
|
+
|
|
404
436
|
export async function completeMcpOAuthCallback(
|
|
405
437
|
deps: OAuthClientDeps,
|
|
406
438
|
input: {
|
|
@@ -446,6 +478,9 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
446
478
|
}
|
|
447
479
|
try {
|
|
448
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);
|
|
449
484
|
if (!input.code) {
|
|
450
485
|
return {
|
|
451
486
|
redirectTo: callbackReturnPath(state.returnPath, "error", {
|
|
@@ -506,6 +541,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
506
541
|
resource: state.resource,
|
|
507
542
|
tokenEndpoint: state.tokenEndpoint,
|
|
508
543
|
client,
|
|
544
|
+
resourceParameterSupported: state.resourceParameterSupported,
|
|
509
545
|
signal,
|
|
510
546
|
}),
|
|
511
547
|
);
|
|
@@ -620,13 +656,13 @@ async function requireOAuthCallbackGrant(db: Database, state: OAuthStatePayload)
|
|
|
620
656
|
}
|
|
621
657
|
}
|
|
622
658
|
|
|
623
|
-
function
|
|
659
|
+
function assertHostedSlackMcpOAuthStart(
|
|
624
660
|
settings: Settings,
|
|
625
661
|
payload: OAuthStartRequest,
|
|
626
662
|
mcpUrl: string,
|
|
627
|
-
|
|
663
|
+
hostedSlackMcp: boolean,
|
|
628
664
|
): void {
|
|
629
|
-
if (!
|
|
665
|
+
if (!hostedSlackMcp) return;
|
|
630
666
|
if (payload.oauthClient) {
|
|
631
667
|
throw new HTTPException(422, {
|
|
632
668
|
message: "Slack OAuth client credentials are deployment-managed",
|
|
@@ -639,13 +675,12 @@ function assertPersonalSlackOAuthStart(
|
|
|
639
675
|
}
|
|
640
676
|
if (!isLocalTestEnvironment(settings.environment) && mcpUrl !== OFFICIAL_SLACK_MCP_URL) {
|
|
641
677
|
throw new HTTPException(422, {
|
|
642
|
-
message: `
|
|
678
|
+
message: `Slack MCP OAuth must use ${OFFICIAL_SLACK_MCP_URL}`,
|
|
643
679
|
});
|
|
644
680
|
}
|
|
645
681
|
if (!settings.slackClientId?.trim() || !settings.slackClientSecret?.trim()) {
|
|
646
682
|
throw new HTTPException(503, {
|
|
647
|
-
message:
|
|
648
|
-
"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",
|
|
649
684
|
});
|
|
650
685
|
}
|
|
651
686
|
}
|
|
@@ -665,6 +700,27 @@ export function assertSlackAuthorizationServer(as: AuthorizationServerMetadata):
|
|
|
665
700
|
}
|
|
666
701
|
}
|
|
667
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
|
+
|
|
668
724
|
async function discoverMcpOAuth(
|
|
669
725
|
resource: string,
|
|
670
726
|
settings: Settings,
|
|
@@ -1154,7 +1210,8 @@ async function existingOAuthConnectionForStart(
|
|
|
1154
1210
|
subjectId: string;
|
|
1155
1211
|
providerDomain: string;
|
|
1156
1212
|
mcpUrl: string;
|
|
1157
|
-
|
|
1213
|
+
hostedSlackMcp: boolean;
|
|
1214
|
+
exactMcpBinding: boolean;
|
|
1158
1215
|
connectionId?: string | undefined;
|
|
1159
1216
|
requestedOwnership?: ConnectionOwnership | undefined;
|
|
1160
1217
|
newConnectionOwnership: ConnectionOwnership;
|
|
@@ -1177,7 +1234,7 @@ async function existingOAuthConnectionForStart(
|
|
|
1177
1234
|
});
|
|
1178
1235
|
}
|
|
1179
1236
|
return connection.providerDomain === input.providerDomain &&
|
|
1180
|
-
(!input.
|
|
1237
|
+
(!input.exactMcpBinding || connection.metadata.mcpUrl === input.mcpUrl)
|
|
1181
1238
|
? connection
|
|
1182
1239
|
: null;
|
|
1183
1240
|
}
|
|
@@ -1188,9 +1245,9 @@ async function existingOAuthConnectionForStart(
|
|
|
1188
1245
|
connection.subjectId === ownerSubjectId &&
|
|
1189
1246
|
connection.kind === "oauth2" &&
|
|
1190
1247
|
connection.providerDomain === input.providerDomain &&
|
|
1191
|
-
(!input.
|
|
1248
|
+
(!input.exactMcpBinding || connection.metadata.mcpUrl === input.mcpUrl),
|
|
1192
1249
|
);
|
|
1193
|
-
if (input.
|
|
1250
|
+
if (input.hostedSlackMcp && input.newConnectionOwnership === "personal") {
|
|
1194
1251
|
return selectCanonicalPersonalSlackConnection(matching);
|
|
1195
1252
|
}
|
|
1196
1253
|
return matching.find((connection) => connection.status === "active") ?? null;
|
|
@@ -1205,7 +1262,7 @@ function ownershipForConnection(
|
|
|
1205
1262
|
throw new HTTPException(404, { message: "connection not found" });
|
|
1206
1263
|
}
|
|
1207
1264
|
|
|
1208
|
-
function buildAuthorizationUrl(input: {
|
|
1265
|
+
export function buildAuthorizationUrl(input: {
|
|
1209
1266
|
endpoint: string;
|
|
1210
1267
|
settings: Settings;
|
|
1211
1268
|
clientId: string;
|
|
@@ -1214,6 +1271,7 @@ function buildAuthorizationUrl(input: {
|
|
|
1214
1271
|
resource: string;
|
|
1215
1272
|
verifier: string;
|
|
1216
1273
|
scopes: string[];
|
|
1274
|
+
resourceParameterSupported: boolean;
|
|
1217
1275
|
}): string {
|
|
1218
1276
|
const endpoint = oauthEndpointUrl(input.endpoint, input.settings, "OAuth authorization endpoint");
|
|
1219
1277
|
const url = new URL(endpoint);
|
|
@@ -1221,7 +1279,16 @@ function buildAuthorizationUrl(input: {
|
|
|
1221
1279
|
url.searchParams.set("client_id", input.clientId);
|
|
1222
1280
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
1223
1281
|
url.searchParams.set("state", input.state);
|
|
1224
|
-
|
|
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
|
+
}
|
|
1225
1292
|
url.searchParams.set("code_challenge_method", "S256");
|
|
1226
1293
|
url.searchParams.set("code_challenge", pkceChallenge(input.verifier));
|
|
1227
1294
|
if (input.scopes.length > 0) {
|
|
@@ -1278,6 +1345,9 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1278
1345
|
),
|
|
1279
1346
|
clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
|
|
1280
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,
|
|
1281
1351
|
...(stringValue(payload.encryptedClientSecret)
|
|
1282
1352
|
? { encryptedClientSecret: stringValue(payload.encryptedClientSecret)! }
|
|
1283
1353
|
: {}),
|
|
@@ -1380,6 +1450,7 @@ async function exchangeAuthorizationCode(
|
|
|
1380
1450
|
verifier: string;
|
|
1381
1451
|
redirectUri: string;
|
|
1382
1452
|
resource: string;
|
|
1453
|
+
resourceParameterSupported: boolean;
|
|
1383
1454
|
tokenEndpoint: string;
|
|
1384
1455
|
client: OAuthClientRegistration;
|
|
1385
1456
|
signal: AbortSignal;
|
|
@@ -1390,7 +1461,9 @@ async function exchangeAuthorizationCode(
|
|
|
1390
1461
|
body.set("code", input.code);
|
|
1391
1462
|
body.set("redirect_uri", input.redirectUri);
|
|
1392
1463
|
body.set("code_verifier", input.verifier);
|
|
1393
|
-
|
|
1464
|
+
if (input.resourceParameterSupported) {
|
|
1465
|
+
body.set("resource", input.resource);
|
|
1466
|
+
}
|
|
1394
1467
|
const headers: Record<string, string> = {
|
|
1395
1468
|
"content-type": "application/x-www-form-urlencoded",
|
|
1396
1469
|
accept: "application/json",
|
|
@@ -1474,31 +1547,16 @@ function throwIfCallbackAborted(signal: AbortSignal, stage: OAuthCallbackStage):
|
|
|
1474
1547
|
function logOAuthCallbackFailure(
|
|
1475
1548
|
observability: Observability | undefined,
|
|
1476
1549
|
error: OAuthCallbackStageError,
|
|
1477
|
-
|
|
1550
|
+
_state: OAuthStatePayload | null,
|
|
1478
1551
|
): void {
|
|
1479
|
-
observability?.error("MCP OAuth callback failed",
|
|
1480
|
-
"opengeni.oauth.stage": error.stage,
|
|
1481
|
-
"opengeni.oauth.reason": error.reason,
|
|
1482
|
-
"opengeni.oauth.provider_domain": state?.providerDomain,
|
|
1483
|
-
"opengeni.oauth.resource_host": state ? safeHost(state.resource) : undefined,
|
|
1484
|
-
"opengeni.oauth.authorization_server": state?.authorizationServer,
|
|
1485
|
-
"opengeni.oauth.issuer": state?.issuer,
|
|
1486
|
-
"opengeni.oauth.client_registration_method": state?.clientRegistrationMethod,
|
|
1487
|
-
error: sanitizedError(error.cause),
|
|
1488
|
-
});
|
|
1552
|
+
observability?.error("MCP OAuth callback failed", oauthPublicErrorFields(error.cause));
|
|
1489
1553
|
}
|
|
1490
1554
|
|
|
1491
1555
|
function logOAuthStartFailure(
|
|
1492
1556
|
observability: Observability | undefined,
|
|
1493
1557
|
error: OAuthStartStageError,
|
|
1494
|
-
providerDomain: string | undefined,
|
|
1495
1558
|
): void {
|
|
1496
|
-
observability?.warn("MCP OAuth setup failed",
|
|
1497
|
-
"opengeni.oauth.stage": error.stage,
|
|
1498
|
-
"opengeni.oauth.reason": error.reason,
|
|
1499
|
-
"opengeni.oauth.provider_domain": providerDomain,
|
|
1500
|
-
error: sanitizedError(error.cause),
|
|
1501
|
-
});
|
|
1559
|
+
observability?.warn("MCP OAuth setup failed", oauthPublicErrorFields(error.cause));
|
|
1502
1560
|
}
|
|
1503
1561
|
|
|
1504
1562
|
function oauthStartFailureReason(error: unknown): string {
|
|
@@ -1528,7 +1586,11 @@ function oauthCallbackFailureReason(stage: OAuthCallbackStage, error: unknown):
|
|
|
1528
1586
|
function isDatabaseStatementTimeout(error: unknown): boolean {
|
|
1529
1587
|
let current = error;
|
|
1530
1588
|
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
1531
|
-
const candidate = current as {
|
|
1589
|
+
const candidate = current as {
|
|
1590
|
+
code?: unknown;
|
|
1591
|
+
message?: unknown;
|
|
1592
|
+
cause?: unknown;
|
|
1593
|
+
};
|
|
1532
1594
|
if (
|
|
1533
1595
|
candidate.code === "57014" ||
|
|
1534
1596
|
(typeof candidate.message === "string" &&
|
|
@@ -1581,51 +1643,47 @@ function oauthStartStageLabel(stage: OAuthStartStage): string {
|
|
|
1581
1643
|
function logOAuthVerificationWarning(
|
|
1582
1644
|
observability: Observability | undefined,
|
|
1583
1645
|
error: OAuthCallbackStageError,
|
|
1584
|
-
|
|
1646
|
+
_state: OAuthStatePayload,
|
|
1585
1647
|
): void {
|
|
1586
|
-
observability?.warn(
|
|
1587
|
-
"
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
"opengeni.oauth.resource_host": safeHost(state.resource),
|
|
1591
|
-
"opengeni.oauth.mcp_host": safeHost(state.mcpUrl),
|
|
1592
|
-
"opengeni.oauth.authorization_server": state.authorizationServer,
|
|
1593
|
-
"opengeni.oauth.issuer": state.issuer,
|
|
1594
|
-
"opengeni.oauth.client_registration_method": state.clientRegistrationMethod,
|
|
1595
|
-
error: sanitizedError(error.cause),
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function sanitizedError(error: unknown): string {
|
|
1600
|
-
if (error instanceof HTTPException) {
|
|
1601
|
-
return `HTTPException ${error.status}: ${error.message}`;
|
|
1602
|
-
}
|
|
1603
|
-
if (error instanceof Error) {
|
|
1604
|
-
return `${error.name}: ${error.message}`;
|
|
1605
|
-
}
|
|
1606
|
-
return String(error);
|
|
1648
|
+
observability?.warn(
|
|
1649
|
+
"MCP OAuth tools/list verification failed after token exchange",
|
|
1650
|
+
oauthPublicErrorFields(error.cause),
|
|
1651
|
+
);
|
|
1607
1652
|
}
|
|
1608
1653
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1654
|
+
export type OAuthPublicErrorFields = {
|
|
1655
|
+
errorClass: "OAuthOperationError";
|
|
1656
|
+
errorCode: "oauth_operation_failed";
|
|
1657
|
+
status?: number;
|
|
1658
|
+
origin: "oauth";
|
|
1659
|
+
};
|
|
1612
1660
|
|
|
1613
|
-
|
|
1661
|
+
/** Allowlisted projection for public telemetry; canonical OAuth errors stay exact. */
|
|
1662
|
+
export function oauthPublicErrorFields(error: unknown): OAuthPublicErrorFields {
|
|
1663
|
+
const fields: OAuthPublicErrorFields = {
|
|
1664
|
+
errorClass: "OAuthOperationError",
|
|
1665
|
+
errorCode: "oauth_operation_failed",
|
|
1666
|
+
origin: "oauth",
|
|
1667
|
+
};
|
|
1614
1668
|
try {
|
|
1615
|
-
|
|
1669
|
+
const rawStatus =
|
|
1670
|
+
error instanceof HTTPException
|
|
1671
|
+
? error.status
|
|
1672
|
+
: error && typeof error === "object"
|
|
1673
|
+
? ((error as { status?: unknown; statusCode?: unknown }).status ??
|
|
1674
|
+
(error as { statusCode?: unknown }).statusCode)
|
|
1675
|
+
: undefined;
|
|
1676
|
+
const status = Number(rawStatus);
|
|
1677
|
+
if (Number.isInteger(status) && status >= 100 && status <= 599) fields.status = status;
|
|
1616
1678
|
} catch {
|
|
1617
|
-
|
|
1679
|
+
// Public telemetry is best-effort. A hostile getter/proxy must never
|
|
1680
|
+
// replace the exact OAuth failure with a projection failure.
|
|
1618
1681
|
}
|
|
1682
|
+
return fields;
|
|
1619
1683
|
}
|
|
1620
1684
|
|
|
1621
|
-
function
|
|
1622
|
-
|
|
1623
|
-
const resource = payload.mcpUrl ?? payload.resource;
|
|
1624
|
-
if (!resource) return undefined;
|
|
1625
|
-
return canonicalProviderDomain(payload.providerDomain ?? new URL(resource).hostname);
|
|
1626
|
-
} catch {
|
|
1627
|
-
return undefined;
|
|
1628
|
-
}
|
|
1685
|
+
function errorMessage(error: unknown): string {
|
|
1686
|
+
return error instanceof Error ? error.message : String(error);
|
|
1629
1687
|
}
|
|
1630
1688
|
|
|
1631
1689
|
async function oauthErrorFromResponse(
|
|
@@ -1747,6 +1805,7 @@ function credentialBundle(
|
|
|
1747
1805
|
token_type: token.tokenType,
|
|
1748
1806
|
...(token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {}),
|
|
1749
1807
|
resource: state.resource,
|
|
1808
|
+
resource_parameter_supported: state.resourceParameterSupported,
|
|
1750
1809
|
mcp_url: state.mcpUrl,
|
|
1751
1810
|
...(token.scopeText
|
|
1752
1811
|
? { scope: token.scopeText }
|
|
@@ -1996,6 +2055,17 @@ function chooseAuthorizeScopes(
|
|
|
1996
2055
|
return uniqueStrings(supported);
|
|
1997
2056
|
}
|
|
1998
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
|
+
|
|
1999
2069
|
function grantedScopes(scopeText: string | undefined, fallback: string[]): string[] {
|
|
2000
2070
|
if (scopeText) {
|
|
2001
2071
|
return uniqueStrings(scopeText.split(/\s+/).filter(Boolean));
|