@opengeni/api-router 2.6.4 → 2.10.0-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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
|
@@ -31,12 +31,20 @@ import {
|
|
|
31
31
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
32
32
|
import {
|
|
33
33
|
DestinationPolicyError,
|
|
34
|
+
McpOAuthDiscoveryError,
|
|
34
35
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
35
36
|
RequestDeadlineError,
|
|
36
37
|
isLocalTestEnvironment,
|
|
38
|
+
parseMcpOAuthChallenge,
|
|
37
39
|
pinnedFetch,
|
|
38
40
|
readResponseJsonBounded,
|
|
41
|
+
resolveMcpOAuthDiscovery,
|
|
39
42
|
validateHttpUrl,
|
|
43
|
+
type McpAuthorizationServerMetadata,
|
|
44
|
+
type McpOAuthChallenge,
|
|
45
|
+
type McpOAuthDiscoveryMode,
|
|
46
|
+
type McpOAuthMetadataFetchResult,
|
|
47
|
+
type McpProtectedResourceMetadata,
|
|
40
48
|
} from "@opengeni/network";
|
|
41
49
|
import { Buffer } from "node:buffer";
|
|
42
50
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -100,30 +108,9 @@ export type OAuthCallbackResult = {
|
|
|
100
108
|
redirectTo: string;
|
|
101
109
|
};
|
|
102
110
|
|
|
103
|
-
type WwwAuthenticateChallenge =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
error?: string;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
type ProtectedResourceMetadata = {
|
|
110
|
-
resource?: string;
|
|
111
|
-
authorizationServers: string[];
|
|
112
|
-
scopesSupported: string[];
|
|
113
|
-
raw: Record<string, unknown>;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
type AuthorizationServerMetadata = {
|
|
117
|
-
issuer: string;
|
|
118
|
-
authorizationServer: string;
|
|
119
|
-
authorizationEndpoint: string;
|
|
120
|
-
tokenEndpoint: string;
|
|
121
|
-
registrationEndpoint?: string;
|
|
122
|
-
clientIdMetadataDocumentSupported: boolean;
|
|
123
|
-
tokenEndpointAuthMethodsSupported: string[];
|
|
124
|
-
codeChallengeMethodsSupported: string[];
|
|
125
|
-
raw: Record<string, unknown>;
|
|
126
|
-
};
|
|
111
|
+
type WwwAuthenticateChallenge = McpOAuthChallenge;
|
|
112
|
+
type ProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
113
|
+
type AuthorizationServerMetadata = McpAuthorizationServerMetadata;
|
|
127
114
|
|
|
128
115
|
type OAuthClientRegistration = {
|
|
129
116
|
method: "operator" | "manual" | "cimd" | "dcr";
|
|
@@ -151,6 +138,10 @@ type OAuthStatePayload = {
|
|
|
151
138
|
tokenEndpoint: string;
|
|
152
139
|
authorizationServer: string;
|
|
153
140
|
issuer: string;
|
|
141
|
+
discoveryMode: McpOAuthDiscoveryMode;
|
|
142
|
+
discoveryMetadataSha256?: string;
|
|
143
|
+
protectedResourceMetadataUrl?: string;
|
|
144
|
+
authorizationServerMetadataUrl?: string;
|
|
154
145
|
clientRegistrationMethod: OAuthClientRegistration["method"];
|
|
155
146
|
tokenEndpointAuthMethod: OAuthClientRegistration["tokenEndpointAuthMethod"];
|
|
156
147
|
resourceParameterSupported: boolean;
|
|
@@ -389,8 +380,9 @@ async function startMcpOAuthWithinDeadline(
|
|
|
389
380
|
|
|
390
381
|
const discovery = await discoverMcpOAuth(mcpUrl, settings, deadline);
|
|
391
382
|
assertDiscoveredAuthorizationServer(settings, discovery.as, profile, providerDomain);
|
|
392
|
-
const resourceParameterSupported =
|
|
393
|
-
|
|
383
|
+
const resourceParameterSupported =
|
|
384
|
+
discovery.mode === "rfc9728_protected_resource" && profile.sendResourceParameter;
|
|
385
|
+
const resource = discovery.resource;
|
|
394
386
|
const verifier = randomPkceVerifier();
|
|
395
387
|
// A profile's exact scope override wins outright: the reviewed connector
|
|
396
388
|
// never lets a caller widen the capability contract.
|
|
@@ -433,6 +425,14 @@ async function startMcpOAuthWithinDeadline(
|
|
|
433
425
|
tokenEndpoint: discovery.as.tokenEndpoint,
|
|
434
426
|
authorizationServer: client.authorizationServer,
|
|
435
427
|
issuer: client.issuer,
|
|
428
|
+
discoveryMode: discovery.mode,
|
|
429
|
+
discoveryMetadataSha256: discovery.provenance.metadataSha256,
|
|
430
|
+
...(discovery.provenance.protectedResourceMetadataUrl
|
|
431
|
+
? {
|
|
432
|
+
protectedResourceMetadataUrl: discovery.provenance.protectedResourceMetadataUrl,
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
435
|
+
authorizationServerMetadataUrl: discovery.provenance.authorizationServerMetadataUrl,
|
|
436
436
|
clientRegistrationMethod: client.method,
|
|
437
437
|
tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
|
|
438
438
|
resourceParameterSupported,
|
|
@@ -711,6 +711,18 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
711
711
|
tokenEndpoint: state.tokenEndpoint,
|
|
712
712
|
clientId: client.clientId,
|
|
713
713
|
clientRegistrationMethod: state.clientRegistrationMethod,
|
|
714
|
+
oauthDiscovery: {
|
|
715
|
+
mode: state.discoveryMode,
|
|
716
|
+
resource: state.resource,
|
|
717
|
+
issuer: state.issuer,
|
|
718
|
+
...(state.discoveryMetadataSha256 ? { metadataSha256: state.discoveryMetadataSha256 } : {}),
|
|
719
|
+
...(state.protectedResourceMetadataUrl
|
|
720
|
+
? { protectedResourceMetadataUrl: state.protectedResourceMetadataUrl }
|
|
721
|
+
: {}),
|
|
722
|
+
...(state.authorizationServerMetadataUrl
|
|
723
|
+
? { authorizationServerMetadataUrl: state.authorizationServerMetadataUrl }
|
|
724
|
+
: {}),
|
|
725
|
+
},
|
|
714
726
|
mcpToolsVerification: verification.metadata,
|
|
715
727
|
...(verification.tools ? { mcpTools: verification.tools } : {}),
|
|
716
728
|
};
|
|
@@ -835,28 +847,50 @@ async function discoverMcpOAuth(
|
|
|
835
847
|
challenge: WwwAuthenticateChallenge;
|
|
836
848
|
prm: ProtectedResourceMetadata;
|
|
837
849
|
as: AuthorizationServerMetadata;
|
|
850
|
+
mode: McpOAuthDiscoveryMode;
|
|
851
|
+
resource: string;
|
|
852
|
+
provenance: {
|
|
853
|
+
protectedResourceMetadataUrl: string | null;
|
|
854
|
+
authorizationServerMetadataUrl: string;
|
|
855
|
+
metadataSha256: string;
|
|
856
|
+
};
|
|
838
857
|
}> {
|
|
839
858
|
const challenge = await deadline.run("mcp_challenge", (signal) =>
|
|
840
859
|
probeMcpChallenge(resource, settings, signal),
|
|
841
860
|
);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
throw new HTTPException(422, {
|
|
856
|
-
message: "authorization server does not support required PKCE S256",
|
|
861
|
+
try {
|
|
862
|
+
const discovery = await resolveMcpOAuthDiscovery({
|
|
863
|
+
resourceUrl: resource,
|
|
864
|
+
challenge,
|
|
865
|
+
fetchMetadata: ({ kind, url }) =>
|
|
866
|
+
deadline.run(
|
|
867
|
+
kind === "protected_resource"
|
|
868
|
+
? "protected_resource_metadata"
|
|
869
|
+
: "authorization_server_metadata",
|
|
870
|
+
(signal) => fetchOAuthMetadata(url, settings, signal),
|
|
871
|
+
),
|
|
872
|
+
validateEndpoint: (rawUrl, label) => oauthEndpointUrl(rawUrl, settings, label),
|
|
873
|
+
canonicalizeResource: canonicalOAuthResource,
|
|
857
874
|
});
|
|
875
|
+
return {
|
|
876
|
+
challenge: discovery.challenge,
|
|
877
|
+
prm: discovery.protectedResourceMetadata,
|
|
878
|
+
as: discovery.authorizationServerMetadata,
|
|
879
|
+
mode: discovery.mode,
|
|
880
|
+
resource: discovery.resource,
|
|
881
|
+
provenance: discovery.provenance,
|
|
882
|
+
};
|
|
883
|
+
} catch (error) {
|
|
884
|
+
if (error instanceof OAuthStartStageError) throw error;
|
|
885
|
+
if (error instanceof McpOAuthDiscoveryError) {
|
|
886
|
+
throw new OAuthStartStageError(
|
|
887
|
+
error.stage,
|
|
888
|
+
error.classification,
|
|
889
|
+
new HTTPException(422, { message: error.message }),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
throw error;
|
|
858
893
|
}
|
|
859
|
-
return { challenge, prm, as };
|
|
860
894
|
}
|
|
861
895
|
|
|
862
896
|
async function probeMcpChallenge(
|
|
@@ -871,115 +905,45 @@ async function probeMcpChallenge(
|
|
|
871
905
|
});
|
|
872
906
|
try {
|
|
873
907
|
if (response.status !== 401) {
|
|
874
|
-
return {};
|
|
908
|
+
return { scheme: null, scope: [] };
|
|
875
909
|
}
|
|
876
|
-
return
|
|
910
|
+
return parseMcpOAuthChallenge(response.headers.get("www-authenticate"));
|
|
877
911
|
} finally {
|
|
878
912
|
await cancelResponseBody(response);
|
|
879
913
|
}
|
|
880
914
|
}
|
|
881
915
|
|
|
882
|
-
async function
|
|
883
|
-
|
|
884
|
-
settings: Settings,
|
|
885
|
-
advertisedUrl?: string,
|
|
886
|
-
signal?: AbortSignal,
|
|
887
|
-
): Promise<ProtectedResourceMetadata> {
|
|
888
|
-
const candidates = uniqueStrings([
|
|
889
|
-
...(advertisedUrl ? [advertisedUrl] : []),
|
|
890
|
-
...wellKnownCandidates(resource, "oauth-protected-resource"),
|
|
891
|
-
]);
|
|
892
|
-
for (const candidate of candidates) {
|
|
893
|
-
const payload = await fetchJsonObject(candidate, settings, signal).catch((error) => {
|
|
894
|
-
if (error instanceof HTTPException) {
|
|
895
|
-
throw error;
|
|
896
|
-
}
|
|
897
|
-
return null;
|
|
898
|
-
});
|
|
899
|
-
if (!payload) {
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
const authorizationServers = stringArray(payload.authorization_servers);
|
|
903
|
-
if (authorizationServers.length === 0) {
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
return {
|
|
907
|
-
authorizationServers,
|
|
908
|
-
scopesSupported: stringArray(payload.scopes_supported),
|
|
909
|
-
raw: payload,
|
|
910
|
-
...(stringValue(payload.resource) ? { resource: stringValue(payload.resource)! } : {}),
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
throw new HTTPException(422, {
|
|
914
|
-
message: "could not discover MCP protected resource metadata",
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
async function discoverAuthorizationServerMetadata(
|
|
919
|
-
authorizationServer: string,
|
|
916
|
+
async function fetchOAuthMetadata(
|
|
917
|
+
url: string,
|
|
920
918
|
settings: Settings,
|
|
921
919
|
signal: AbortSignal,
|
|
922
|
-
): Promise<
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
"OAuth authorization server",
|
|
927
|
-
).replace(/\/+$/, "");
|
|
928
|
-
const candidates = uniqueStrings([
|
|
929
|
-
// Prefer the RFC metadata locations before probing the issuer itself. Some
|
|
930
|
-
// providers (including Linear) redirect their issuer root to a human docs
|
|
931
|
-
// page; following that redirect can leave discovery waiting on an unrelated
|
|
932
|
-
// streaming response even though the well-known metadata is immediately
|
|
933
|
-
// available.
|
|
934
|
-
...wellKnownCandidates(safeAuthorizationServer, "oauth-authorization-server"),
|
|
935
|
-
...wellKnownCandidates(safeAuthorizationServer, "openid-configuration"),
|
|
936
|
-
safeAuthorizationServer,
|
|
937
|
-
]);
|
|
938
|
-
for (const candidate of candidates) {
|
|
939
|
-
const payload = await fetchJsonObject(candidate, settings, signal).catch((error) => {
|
|
940
|
-
if (error instanceof HTTPException) {
|
|
941
|
-
throw error;
|
|
942
|
-
}
|
|
943
|
-
return null;
|
|
944
|
-
});
|
|
945
|
-
if (!payload) {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
const authorizationEndpoint = stringValue(payload.authorization_endpoint);
|
|
949
|
-
const tokenEndpoint = stringValue(payload.token_endpoint);
|
|
950
|
-
if (!authorizationEndpoint || !tokenEndpoint) {
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
const safeAuthorizationEndpoint = oauthEndpointUrl(
|
|
954
|
-
authorizationEndpoint,
|
|
955
|
-
settings,
|
|
956
|
-
"OAuth authorization endpoint",
|
|
957
|
-
);
|
|
958
|
-
const safeTokenEndpoint = oauthEndpointUrl(tokenEndpoint, settings, "OAuth token endpoint");
|
|
959
|
-
const registrationEndpoint = stringValue(payload.registration_endpoint);
|
|
960
|
-
const issuer = oauthEndpointUrl(
|
|
961
|
-
stringValue(payload.issuer) ?? safeAuthorizationServer,
|
|
962
|
-
settings,
|
|
963
|
-
"OAuth issuer",
|
|
964
|
-
);
|
|
965
|
-
const safeRegistrationEndpoint = registrationEndpoint
|
|
966
|
-
? oauthEndpointUrl(registrationEndpoint, settings, "OAuth registration endpoint")
|
|
967
|
-
: undefined;
|
|
968
|
-
return {
|
|
969
|
-
issuer,
|
|
970
|
-
authorizationServer: safeAuthorizationServer,
|
|
971
|
-
authorizationEndpoint: safeAuthorizationEndpoint,
|
|
972
|
-
tokenEndpoint: safeTokenEndpoint,
|
|
973
|
-
clientIdMetadataDocumentSupported: payload.client_id_metadata_document_supported === true,
|
|
974
|
-
tokenEndpointAuthMethodsSupported: stringArray(payload.token_endpoint_auth_methods_supported),
|
|
975
|
-
codeChallengeMethodsSupported: stringArray(payload.code_challenge_methods_supported),
|
|
976
|
-
raw: payload,
|
|
977
|
-
...(safeRegistrationEndpoint ? { registrationEndpoint: safeRegistrationEndpoint } : {}),
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
throw new HTTPException(422, {
|
|
981
|
-
message: "could not discover OAuth authorization server metadata",
|
|
920
|
+
): Promise<McpOAuthMetadataFetchResult> {
|
|
921
|
+
const response = await fetchOAuth(url, settings, {
|
|
922
|
+
headers: { accept: "application/json" },
|
|
923
|
+
signal,
|
|
982
924
|
});
|
|
925
|
+
if (response.status === 404 || response.status === 410) {
|
|
926
|
+
await cancelResponseBody(response);
|
|
927
|
+
return { status: "absent", url, httpStatus: response.status };
|
|
928
|
+
}
|
|
929
|
+
if (!response.ok) {
|
|
930
|
+
await cancelResponseBody(response);
|
|
931
|
+
throw new Error(`OAuth metadata endpoint returned HTTP ${response.status}`);
|
|
932
|
+
}
|
|
933
|
+
const payload = await readResponseJsonBounded<unknown>(
|
|
934
|
+
response,
|
|
935
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
936
|
+
"OAuth metadata response",
|
|
937
|
+
{ signal },
|
|
938
|
+
);
|
|
939
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
940
|
+
throw new Error("OAuth metadata response was not a JSON object");
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
status: "present",
|
|
944
|
+
url,
|
|
945
|
+
document: payload as Record<string, unknown>,
|
|
946
|
+
};
|
|
983
947
|
}
|
|
984
948
|
|
|
985
949
|
async function registerOAuthClient(
|
|
@@ -1433,6 +1397,29 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1433
1397
|
throw new HTTPException(400, { message: "invalid or expired OAuth state" });
|
|
1434
1398
|
}
|
|
1435
1399
|
const resource = requiredString(payload.resource, "state.resource");
|
|
1400
|
+
const encodedDiscoveryMode = stringValue(payload.discoveryMode);
|
|
1401
|
+
const discoveryMode = discoveryModeValue(encodedDiscoveryMode);
|
|
1402
|
+
const discoveryMetadataSha256 = stringValue(payload.discoveryMetadataSha256);
|
|
1403
|
+
if (
|
|
1404
|
+
encodedDiscoveryMode &&
|
|
1405
|
+
(!discoveryMetadataSha256 || !/^[0-9a-f]{64}$/.test(discoveryMetadataSha256))
|
|
1406
|
+
) {
|
|
1407
|
+
throw new HTTPException(400, { message: "invalid OAuth discovery state" });
|
|
1408
|
+
}
|
|
1409
|
+
const protectedResourceMetadataUrl = stringValue(payload.protectedResourceMetadataUrl);
|
|
1410
|
+
const authorizationServerMetadataUrl = stringValue(payload.authorizationServerMetadataUrl);
|
|
1411
|
+
if (
|
|
1412
|
+
encodedDiscoveryMode &&
|
|
1413
|
+
(!authorizationServerMetadataUrl ||
|
|
1414
|
+
(discoveryMode === "rfc9728_protected_resource" && !protectedResourceMetadataUrl) ||
|
|
1415
|
+
(discoveryMode === "legacy_2025_03_26_metadata" && protectedResourceMetadataUrl))
|
|
1416
|
+
) {
|
|
1417
|
+
throw new HTTPException(400, { message: "invalid OAuth discovery provenance state" });
|
|
1418
|
+
}
|
|
1419
|
+
const resourceParameterSupported = payload.resourceParameterSupported !== false;
|
|
1420
|
+
if (discoveryMode === "legacy_2025_03_26_metadata" && resourceParameterSupported) {
|
|
1421
|
+
throw new HTTPException(400, { message: "invalid legacy OAuth resource parameter state" });
|
|
1422
|
+
}
|
|
1436
1423
|
const parsed = {
|
|
1437
1424
|
accountId: requiredString(payload.accountId, "state.accountId"),
|
|
1438
1425
|
workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
|
|
@@ -1467,11 +1454,31 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1467
1454
|
settings,
|
|
1468
1455
|
"OAuth issuer",
|
|
1469
1456
|
),
|
|
1457
|
+
discoveryMode,
|
|
1458
|
+
...(discoveryMetadataSha256 ? { discoveryMetadataSha256 } : {}),
|
|
1459
|
+
...(protectedResourceMetadataUrl
|
|
1460
|
+
? {
|
|
1461
|
+
protectedResourceMetadataUrl: oauthEndpointUrl(
|
|
1462
|
+
protectedResourceMetadataUrl,
|
|
1463
|
+
settings,
|
|
1464
|
+
"OAuth protected resource metadata",
|
|
1465
|
+
),
|
|
1466
|
+
}
|
|
1467
|
+
: {}),
|
|
1468
|
+
...(authorizationServerMetadataUrl
|
|
1469
|
+
? {
|
|
1470
|
+
authorizationServerMetadataUrl: oauthEndpointUrl(
|
|
1471
|
+
authorizationServerMetadataUrl,
|
|
1472
|
+
settings,
|
|
1473
|
+
"OAuth authorization server metadata",
|
|
1474
|
+
),
|
|
1475
|
+
}
|
|
1476
|
+
: {}),
|
|
1470
1477
|
clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
|
|
1471
1478
|
tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.tokenEndpointAuthMethod), false),
|
|
1472
1479
|
// States minted before provider-specific compatibility was introduced used
|
|
1473
1480
|
// the RFC 8707 resource parameter.
|
|
1474
|
-
resourceParameterSupported
|
|
1481
|
+
resourceParameterSupported,
|
|
1475
1482
|
...(stringValue(payload.encryptedClientSecret)
|
|
1476
1483
|
? { encryptedClientSecret: stringValue(payload.encryptedClientSecret)! }
|
|
1477
1484
|
: {}),
|
|
@@ -1484,6 +1491,13 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1484
1491
|
if (Boolean(connectionId) !== Boolean(connectionVersion)) {
|
|
1485
1492
|
throw new HTTPException(400, { message: "invalid OAuth reconnect state" });
|
|
1486
1493
|
}
|
|
1494
|
+
if (
|
|
1495
|
+
parsed.discoveryMode === "legacy_2025_03_26_metadata" &&
|
|
1496
|
+
(normalizedIssuerKey(parsed.authorizationServer) !== normalizedIssuerKey(parsed.issuer) ||
|
|
1497
|
+
new URL(parsed.resource).origin !== new URL(parsed.issuer).origin)
|
|
1498
|
+
) {
|
|
1499
|
+
throw new HTTPException(400, { message: "invalid OAuth discovery binding state" });
|
|
1500
|
+
}
|
|
1487
1501
|
return {
|
|
1488
1502
|
...parsed,
|
|
1489
1503
|
...(connectionId ? { connectionId } : {}),
|
|
@@ -1491,6 +1505,17 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1491
1505
|
};
|
|
1492
1506
|
}
|
|
1493
1507
|
|
|
1508
|
+
function discoveryModeValue(value: string | undefined): McpOAuthDiscoveryMode {
|
|
1509
|
+
if (!value) {
|
|
1510
|
+
// Every state minted before discovery modes existed used RFC 9728 PRM.
|
|
1511
|
+
return "rfc9728_protected_resource";
|
|
1512
|
+
}
|
|
1513
|
+
if (value === "rfc9728_protected_resource" || value === "legacy_2025_03_26_metadata") {
|
|
1514
|
+
return value;
|
|
1515
|
+
}
|
|
1516
|
+
throw new HTTPException(400, { message: "invalid OAuth discovery mode state" });
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1494
1519
|
function connectionOwnership(value: unknown): ConnectionOwnership | undefined {
|
|
1495
1520
|
return value === "workspace" || value === "personal" ? value : undefined;
|
|
1496
1521
|
}
|
|
@@ -2033,31 +2058,6 @@ function safeReturnPath(value: string): string {
|
|
|
2033
2058
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
2034
2059
|
}
|
|
2035
2060
|
|
|
2036
|
-
async function fetchJsonObject(
|
|
2037
|
-
url: string,
|
|
2038
|
-
settings: Settings,
|
|
2039
|
-
signal?: AbortSignal,
|
|
2040
|
-
): Promise<Record<string, unknown>> {
|
|
2041
|
-
const response = await fetchOAuth(url, settings, {
|
|
2042
|
-
headers: { accept: "application/json" },
|
|
2043
|
-
...(signal ? { signal } : {}),
|
|
2044
|
-
});
|
|
2045
|
-
if (!response.ok) {
|
|
2046
|
-
await cancelResponseBody(response);
|
|
2047
|
-
throw new Error(`HTTP ${response.status}`);
|
|
2048
|
-
}
|
|
2049
|
-
const payload = await readResponseJsonBounded<unknown>(
|
|
2050
|
-
response,
|
|
2051
|
-
OAUTH_MAX_RESPONSE_BYTES,
|
|
2052
|
-
"OAuth metadata response",
|
|
2053
|
-
{ ...(signal ? { signal } : {}) },
|
|
2054
|
-
);
|
|
2055
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2056
|
-
throw new Error("metadata response was not a JSON object");
|
|
2057
|
-
}
|
|
2058
|
-
return payload as Record<string, unknown>;
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
2061
|
async function fetchOAuth(
|
|
2062
2062
|
rawUrl: string,
|
|
2063
2063
|
settings: Settings,
|
|
@@ -2130,41 +2130,6 @@ async function cancelResponseBody(response: Response): Promise<void> {
|
|
|
2130
2130
|
await response.body?.cancel().catch(() => undefined);
|
|
2131
2131
|
}
|
|
2132
2132
|
|
|
2133
|
-
function parseWwwAuthenticate(header: string | null): WwwAuthenticateChallenge {
|
|
2134
|
-
if (!header) {
|
|
2135
|
-
return {};
|
|
2136
|
-
}
|
|
2137
|
-
const bearerIndex = header.toLowerCase().indexOf("bearer");
|
|
2138
|
-
if (bearerIndex < 0) {
|
|
2139
|
-
return {};
|
|
2140
|
-
}
|
|
2141
|
-
const paramsText = header.slice(bearerIndex + "bearer".length);
|
|
2142
|
-
const params: Record<string, string> = {};
|
|
2143
|
-
const re = /([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*("(?:[^"\\]|\\.)*"|[^,\s]+)/g;
|
|
2144
|
-
let match: RegExpExecArray | null;
|
|
2145
|
-
while ((match = re.exec(paramsText)) !== null) {
|
|
2146
|
-
const raw = match[2]!;
|
|
2147
|
-
params[match[1]!.toLowerCase()] = raw.startsWith('"')
|
|
2148
|
-
? raw.slice(1, -1).replace(/\\"/g, '"')
|
|
2149
|
-
: raw;
|
|
2150
|
-
}
|
|
2151
|
-
return {
|
|
2152
|
-
...(params.resource_metadata ? { resourceMetadata: params.resource_metadata } : {}),
|
|
2153
|
-
...(params.scope ? { scope: params.scope.split(/\s+/).filter(Boolean) } : {}),
|
|
2154
|
-
...(params.error ? { error: params.error } : {}),
|
|
2155
|
-
};
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
function wellKnownCandidates(rawUrl: string, name: string): string[] {
|
|
2159
|
-
const url = new URL(rawUrl);
|
|
2160
|
-
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
2161
|
-
return uniqueStrings([
|
|
2162
|
-
`${url.origin}/.well-known/${name}${path ? `/${path}` : ""}`,
|
|
2163
|
-
`${url.origin}${path ? `/${path}` : ""}/.well-known/${name}`,
|
|
2164
|
-
`${url.origin}/.well-known/${name}`,
|
|
2165
|
-
]);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
2133
|
function chooseAuthorizeScopes(
|
|
2169
2134
|
requested: string[] | undefined,
|
|
2170
2135
|
challenged: string[] | undefined,
|