@opengeni/api-router 0.5.7 → 0.9.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/app.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
resolveNatsControlPlaneAuth,
|
|
6
6
|
retryStartupDependency,
|
|
7
7
|
startupRetryOptions,
|
|
8
|
+
temporalConnectionOptions,
|
|
8
9
|
} from "@opengeni/config";
|
|
9
10
|
import type {
|
|
10
11
|
ScheduledTask,
|
|
@@ -61,7 +62,7 @@ export async function createTemporalWorkflowClient(
|
|
|
61
62
|
documentIndexer: DocumentIndexClient;
|
|
62
63
|
close: () => Promise<void>;
|
|
63
64
|
}> {
|
|
64
|
-
const connection = await Connection.connect(
|
|
65
|
+
const connection = await Connection.connect(temporalConnectionOptions(settings));
|
|
65
66
|
const temporal = new TemporalClient({
|
|
66
67
|
connection,
|
|
67
68
|
namespace: settings.temporalNamespace,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
decryptEnvironmentValue,
|
|
12
12
|
encryptEnvironmentValue,
|
|
13
13
|
getConnectionMetadata,
|
|
14
|
-
isPrivateAddress,
|
|
15
14
|
listConnectionsMetadata,
|
|
16
15
|
loadIntegrationOAuthClient,
|
|
17
16
|
normalizeBearerScheme,
|
|
@@ -20,14 +19,21 @@ import {
|
|
|
20
19
|
type Database,
|
|
21
20
|
} from "@opengeni/db";
|
|
22
21
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
22
|
+
import {
|
|
23
|
+
DestinationPolicyError,
|
|
24
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
25
|
+
isLocalTestEnvironment,
|
|
26
|
+
pinnedFetch,
|
|
27
|
+
readResponseJsonBounded,
|
|
28
|
+
validateHttpUrl,
|
|
29
|
+
} from "@opengeni/network";
|
|
23
30
|
import { Buffer } from "node:buffer";
|
|
24
31
|
import { createHash, randomBytes } from "node:crypto";
|
|
25
|
-
import { lookup } from "node:dns/promises";
|
|
26
|
-
import { isIP } from "node:net";
|
|
27
32
|
import { HTTPException } from "hono/http-exception";
|
|
28
33
|
import { canonicalProviderDomain } from "./provider-domain";
|
|
29
34
|
|
|
30
35
|
export const oauthStateTtlMs = 10 * 60 * 1000;
|
|
36
|
+
export { OAUTH_MAX_RESPONSE_BYTES } from "@opengeni/network";
|
|
31
37
|
|
|
32
38
|
type OAuthClientDeps = {
|
|
33
39
|
db: Database;
|
|
@@ -192,6 +198,7 @@ export async function startMcpOAuth(
|
|
|
192
198
|
});
|
|
193
199
|
const authorizationUrl = buildAuthorizationUrl({
|
|
194
200
|
endpoint: discovery.as.authorizationEndpoint,
|
|
201
|
+
settings,
|
|
195
202
|
clientId: client.clientId,
|
|
196
203
|
redirectUri,
|
|
197
204
|
state,
|
|
@@ -386,10 +393,14 @@ async function probeMcpChallenge(
|
|
|
386
393
|
method: "GET",
|
|
387
394
|
headers: { accept: "application/json" },
|
|
388
395
|
});
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
try {
|
|
397
|
+
if (response.status !== 401) {
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
return parseWwwAuthenticate(response.headers.get("www-authenticate"));
|
|
401
|
+
} finally {
|
|
402
|
+
await cancelResponseBody(response);
|
|
391
403
|
}
|
|
392
|
-
return parseWwwAuthenticate(response.headers.get("www-authenticate"));
|
|
393
404
|
}
|
|
394
405
|
|
|
395
406
|
async function discoverProtectedResourceMetadata(
|
|
@@ -429,10 +440,15 @@ async function discoverAuthorizationServerMetadata(
|
|
|
429
440
|
authorizationServer: string,
|
|
430
441
|
settings: Settings,
|
|
431
442
|
): Promise<AuthorizationServerMetadata> {
|
|
432
|
-
const
|
|
443
|
+
const safeAuthorizationServer = oauthEndpointUrl(
|
|
433
444
|
authorizationServer,
|
|
434
|
-
|
|
435
|
-
|
|
445
|
+
settings,
|
|
446
|
+
"OAuth authorization server",
|
|
447
|
+
).replace(/\/+$/, "");
|
|
448
|
+
const candidates = uniqueStrings([
|
|
449
|
+
safeAuthorizationServer,
|
|
450
|
+
...wellKnownCandidates(safeAuthorizationServer, "oauth-authorization-server"),
|
|
451
|
+
...wellKnownCandidates(safeAuthorizationServer, "openid-configuration"),
|
|
436
452
|
]);
|
|
437
453
|
for (const candidate of candidates) {
|
|
438
454
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
@@ -449,18 +465,31 @@ async function discoverAuthorizationServerMetadata(
|
|
|
449
465
|
if (!authorizationEndpoint || !tokenEndpoint) {
|
|
450
466
|
continue;
|
|
451
467
|
}
|
|
452
|
-
|
|
453
|
-
issuer: stringValue(payload.issuer) ?? authorizationServer.replace(/\/+$/, ""),
|
|
454
|
-
authorizationServer: authorizationServer.replace(/\/+$/, ""),
|
|
468
|
+
const safeAuthorizationEndpoint = oauthEndpointUrl(
|
|
455
469
|
authorizationEndpoint,
|
|
456
|
-
|
|
470
|
+
settings,
|
|
471
|
+
"OAuth authorization endpoint",
|
|
472
|
+
);
|
|
473
|
+
const safeTokenEndpoint = oauthEndpointUrl(tokenEndpoint, settings, "OAuth token endpoint");
|
|
474
|
+
const registrationEndpoint = stringValue(payload.registration_endpoint);
|
|
475
|
+
const issuer = oauthEndpointUrl(
|
|
476
|
+
stringValue(payload.issuer) ?? safeAuthorizationServer,
|
|
477
|
+
settings,
|
|
478
|
+
"OAuth issuer",
|
|
479
|
+
);
|
|
480
|
+
const safeRegistrationEndpoint = registrationEndpoint
|
|
481
|
+
? oauthEndpointUrl(registrationEndpoint, settings, "OAuth registration endpoint")
|
|
482
|
+
: undefined;
|
|
483
|
+
return {
|
|
484
|
+
issuer,
|
|
485
|
+
authorizationServer: safeAuthorizationServer,
|
|
486
|
+
authorizationEndpoint: safeAuthorizationEndpoint,
|
|
487
|
+
tokenEndpoint: safeTokenEndpoint,
|
|
457
488
|
clientIdMetadataDocumentSupported: payload.client_id_metadata_document_supported === true,
|
|
458
489
|
tokenEndpointAuthMethodsSupported: stringArray(payload.token_endpoint_auth_methods_supported),
|
|
459
490
|
codeChallengeMethodsSupported: stringArray(payload.code_challenge_methods_supported),
|
|
460
491
|
raw: payload,
|
|
461
|
-
...(
|
|
462
|
-
? { registrationEndpoint: stringValue(payload.registration_endpoint)! }
|
|
463
|
-
: {}),
|
|
492
|
+
...(safeRegistrationEndpoint ? { registrationEndpoint: safeRegistrationEndpoint } : {}),
|
|
464
493
|
};
|
|
465
494
|
}
|
|
466
495
|
throw new HTTPException(422, {
|
|
@@ -661,7 +690,6 @@ async function dynamicClientRegistration(
|
|
|
661
690
|
message: "authorization server does not support dynamic client registration",
|
|
662
691
|
});
|
|
663
692
|
}
|
|
664
|
-
await assertOAuthFetchAllowed(as.registrationEndpoint, settings);
|
|
665
693
|
const response = await fetchOAuth(as.registrationEndpoint, settings, {
|
|
666
694
|
method: "POST",
|
|
667
695
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
@@ -675,11 +703,16 @@ async function dynamicClientRegistration(
|
|
|
675
703
|
}),
|
|
676
704
|
});
|
|
677
705
|
if (!response.ok) {
|
|
706
|
+
await cancelResponseBody(response);
|
|
678
707
|
throw new HTTPException(422, {
|
|
679
708
|
message: `dynamic client registration failed with HTTP ${response.status}`,
|
|
680
709
|
});
|
|
681
710
|
}
|
|
682
|
-
const payload =
|
|
711
|
+
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
712
|
+
response,
|
|
713
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
714
|
+
"OAuth dynamic registration response",
|
|
715
|
+
);
|
|
683
716
|
const clientId = stringValue(payload.client_id);
|
|
684
717
|
if (!clientId) {
|
|
685
718
|
throw new HTTPException(422, {
|
|
@@ -726,6 +759,7 @@ async function existingOAuthConnectionForStart(
|
|
|
726
759
|
|
|
727
760
|
function buildAuthorizationUrl(input: {
|
|
728
761
|
endpoint: string;
|
|
762
|
+
settings: Settings;
|
|
729
763
|
clientId: string;
|
|
730
764
|
redirectUri: string;
|
|
731
765
|
state: string;
|
|
@@ -733,7 +767,8 @@ function buildAuthorizationUrl(input: {
|
|
|
733
767
|
verifier: string;
|
|
734
768
|
scopes: string[];
|
|
735
769
|
}): string {
|
|
736
|
-
const
|
|
770
|
+
const endpoint = oauthEndpointUrl(input.endpoint, input.settings, "OAuth authorization endpoint");
|
|
771
|
+
const url = new URL(endpoint);
|
|
737
772
|
url.searchParams.set("response_type", "code");
|
|
738
773
|
url.searchParams.set("client_id", input.clientId);
|
|
739
774
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
@@ -775,9 +810,21 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
775
810
|
"state.encryptedPkceVerifier",
|
|
776
811
|
),
|
|
777
812
|
clientId: requiredString(payload.clientId, "state.clientId"),
|
|
778
|
-
tokenEndpoint:
|
|
779
|
-
|
|
780
|
-
|
|
813
|
+
tokenEndpoint: oauthEndpointUrl(
|
|
814
|
+
requiredString(payload.tokenEndpoint, "state.tokenEndpoint"),
|
|
815
|
+
settings,
|
|
816
|
+
"OAuth token endpoint",
|
|
817
|
+
),
|
|
818
|
+
authorizationServer: oauthEndpointUrl(
|
|
819
|
+
requiredString(payload.authorizationServer, "state.authorizationServer"),
|
|
820
|
+
settings,
|
|
821
|
+
"OAuth authorization server",
|
|
822
|
+
).replace(/\/+$/, ""),
|
|
823
|
+
issuer: oauthEndpointUrl(
|
|
824
|
+
requiredString(payload.issuer, "state.issuer"),
|
|
825
|
+
settings,
|
|
826
|
+
"OAuth issuer",
|
|
827
|
+
),
|
|
781
828
|
clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
|
|
782
829
|
tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.tokenEndpointAuthMethod), false),
|
|
783
830
|
...(stringValue(payload.encryptedClientSecret)
|
|
@@ -825,7 +872,12 @@ async function clientForState(
|
|
|
825
872
|
}
|
|
826
873
|
if (state.clientRegistrationMethod === "dcr") {
|
|
827
874
|
const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
|
|
828
|
-
if (
|
|
875
|
+
if (
|
|
876
|
+
!stored ||
|
|
877
|
+
stored.clientId !== state.clientId ||
|
|
878
|
+
stored.issuer !== state.issuer ||
|
|
879
|
+
stored.authorizationServer !== state.authorizationServer
|
|
880
|
+
) {
|
|
829
881
|
throw new HTTPException(400, { message: "OAuth client registration is no longer available" });
|
|
830
882
|
}
|
|
831
883
|
return {
|
|
@@ -870,7 +922,6 @@ async function exchangeAuthorizationCode(
|
|
|
870
922
|
client: OAuthClientRegistration;
|
|
871
923
|
},
|
|
872
924
|
): Promise<TokenResponse> {
|
|
873
|
-
await assertOAuthFetchAllowed(input.tokenEndpoint, settings);
|
|
874
925
|
const body = new URLSearchParams();
|
|
875
926
|
body.set("grant_type", "authorization_code");
|
|
876
927
|
body.set("code", input.code);
|
|
@@ -905,7 +956,11 @@ async function exchangeAuthorizationCode(
|
|
|
905
956
|
new Error(`OAuth token endpoint returned HTTP ${response.status}`),
|
|
906
957
|
);
|
|
907
958
|
}
|
|
908
|
-
const payload =
|
|
959
|
+
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
960
|
+
response,
|
|
961
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
962
|
+
"OAuth token response",
|
|
963
|
+
);
|
|
909
964
|
const accessToken = stringValue(payload.access_token);
|
|
910
965
|
if (!accessToken) {
|
|
911
966
|
throw new Error("OAuth token response did not include access_token");
|
|
@@ -997,12 +1052,17 @@ function safeHost(rawUrl: string): string | undefined {
|
|
|
997
1052
|
async function oauthErrorFromResponse(response: Response): Promise<string | null> {
|
|
998
1053
|
const contentType = response.headers.get("content-type") ?? "";
|
|
999
1054
|
if (!contentType.toLowerCase().includes("application/json")) {
|
|
1055
|
+
await cancelResponseBody(response);
|
|
1000
1056
|
return null;
|
|
1001
1057
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1058
|
+
// Consume the original response, not a clone. The pinned transport owns a
|
|
1059
|
+
// per-response dispatcher, so leaving the original body unread would retain
|
|
1060
|
+
// its socket pool after a token endpoint error.
|
|
1061
|
+
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
1062
|
+
response,
|
|
1063
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
1064
|
+
"OAuth token error response",
|
|
1065
|
+
).catch(() => null);
|
|
1006
1066
|
const error = stringValue(payload?.error);
|
|
1007
1067
|
if (!error || !/^[a-zA-Z0-9_.-]{1,80}$/.test(error)) {
|
|
1008
1068
|
return null;
|
|
@@ -1015,7 +1075,6 @@ async function verifyMcpToolsList(
|
|
|
1015
1075
|
resource: string,
|
|
1016
1076
|
token: TokenResponse,
|
|
1017
1077
|
): Promise<Array<{ name: string; description?: string }>> {
|
|
1018
|
-
await assertOAuthFetchAllowed(resource, settings);
|
|
1019
1078
|
const client = new Client(
|
|
1020
1079
|
{ name: "opengeni-integration-verify", version: "0.1.0" },
|
|
1021
1080
|
{ capabilities: {} },
|
|
@@ -1158,6 +1217,20 @@ function canonicalOAuthResource(value: string): string {
|
|
|
1158
1217
|
}
|
|
1159
1218
|
}
|
|
1160
1219
|
|
|
1220
|
+
function oauthEndpointUrl(rawUrl: string, settings: Settings, label: string): string {
|
|
1221
|
+
try {
|
|
1222
|
+
return validateHttpUrl(rawUrl, {
|
|
1223
|
+
label,
|
|
1224
|
+
allowLoopbackHttp: isLocalTestEnvironment(settings.environment),
|
|
1225
|
+
});
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (error instanceof DestinationPolicyError) {
|
|
1228
|
+
throw new HTTPException(422, { message: error.message });
|
|
1229
|
+
}
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1161
1234
|
function safeReturnPath(value: string): string {
|
|
1162
1235
|
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
1163
1236
|
throw new HTTPException(400, { message: "OAuth returnPath must be a relative path" });
|
|
@@ -1172,9 +1245,14 @@ function safeReturnPath(value: string): string {
|
|
|
1172
1245
|
async function fetchJsonObject(url: string, settings: Settings): Promise<Record<string, unknown>> {
|
|
1173
1246
|
const response = await fetchOAuth(url, settings, { headers: { accept: "application/json" } });
|
|
1174
1247
|
if (!response.ok) {
|
|
1248
|
+
await cancelResponseBody(response);
|
|
1175
1249
|
throw new Error(`HTTP ${response.status}`);
|
|
1176
1250
|
}
|
|
1177
|
-
const payload = await
|
|
1251
|
+
const payload = await readResponseJsonBounded<unknown>(
|
|
1252
|
+
response,
|
|
1253
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
1254
|
+
"OAuth metadata response",
|
|
1255
|
+
);
|
|
1178
1256
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1179
1257
|
throw new Error("metadata response was not a JSON object");
|
|
1180
1258
|
}
|
|
@@ -1187,56 +1265,64 @@ async function fetchOAuth(
|
|
|
1187
1265
|
init: RequestInit = {},
|
|
1188
1266
|
hop = 0,
|
|
1189
1267
|
): Promise<Response> {
|
|
1190
|
-
|
|
1191
|
-
|
|
1268
|
+
let response: Response;
|
|
1269
|
+
try {
|
|
1270
|
+
const endpoint = oauthEndpointUrl(rawUrl, settings, "OAuth endpoint");
|
|
1271
|
+
response = await pinnedFetch(endpoint, init, settings, {
|
|
1272
|
+
label: "OAuth discovery",
|
|
1273
|
+
requireHttpsOutsideLocalTest: true,
|
|
1274
|
+
});
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (error instanceof DestinationPolicyError) {
|
|
1277
|
+
throw new HTTPException(422, { message: error.message });
|
|
1278
|
+
}
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1192
1281
|
if (response.status < 300 || response.status >= 400) {
|
|
1193
1282
|
return response;
|
|
1194
1283
|
}
|
|
1284
|
+
// Discovery is the only redirectable OAuth traffic. Replaying a token
|
|
1285
|
+
// exchange, dynamic registration, or authenticated MCP request would send
|
|
1286
|
+
// its body and/or credential headers to a provider-controlled Location.
|
|
1287
|
+
// Keep this allowlist deliberately narrow so future credential headers fail
|
|
1288
|
+
// closed instead of silently becoming redirectable.
|
|
1289
|
+
if (!oauthRequestMayFollowRedirect(init)) {
|
|
1290
|
+
await cancelResponseBody(response);
|
|
1291
|
+
throw new HTTPException(422, {
|
|
1292
|
+
message: "OAuth credential-bearing requests may not follow redirects",
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1195
1295
|
if (hop >= 3) {
|
|
1296
|
+
await cancelResponseBody(response);
|
|
1196
1297
|
throw new HTTPException(422, { message: "OAuth fetch exceeded maximum redirect hops" });
|
|
1197
1298
|
}
|
|
1198
1299
|
const location = response.headers.get("location");
|
|
1199
1300
|
if (!location) {
|
|
1301
|
+
await cancelResponseBody(response);
|
|
1200
1302
|
throw new HTTPException(422, { message: "OAuth fetch redirect was missing Location" });
|
|
1201
1303
|
}
|
|
1202
1304
|
let nextUrl: string;
|
|
1203
1305
|
try {
|
|
1204
1306
|
nextUrl = new URL(location, rawUrl).toString();
|
|
1205
1307
|
} catch {
|
|
1308
|
+
await cancelResponseBody(response);
|
|
1206
1309
|
throw new HTTPException(422, { message: "OAuth fetch redirect Location was invalid" });
|
|
1207
1310
|
}
|
|
1311
|
+
await cancelResponseBody(response);
|
|
1208
1312
|
return await fetchOAuth(nextUrl, settings, init, hop + 1);
|
|
1209
1313
|
}
|
|
1210
1314
|
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
if (
|
|
1217
|
-
settings.integrationsAllowPrivateNetworkTargets ||
|
|
1218
|
-
["local", "test"].includes(settings.environment)
|
|
1219
|
-
) {
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
if (url.protocol !== "https:") {
|
|
1223
|
-
throw new HTTPException(422, {
|
|
1224
|
-
message: "OAuth discovery targets must use https outside local/test",
|
|
1225
|
-
});
|
|
1226
|
-
}
|
|
1227
|
-
const hostname = url.hostname.toLowerCase();
|
|
1228
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
1229
|
-
throw new HTTPException(422, { message: "OAuth discovery may not target localhost" });
|
|
1230
|
-
}
|
|
1231
|
-
const literal = isIP(hostname);
|
|
1232
|
-
const addresses = literal
|
|
1233
|
-
? [hostname]
|
|
1234
|
-
: (await lookup(hostname, { all: true })).map((entry) => entry.address);
|
|
1235
|
-
if (addresses.some(isPrivateAddress)) {
|
|
1236
|
-
throw new HTTPException(422, {
|
|
1237
|
-
message: "OAuth discovery may not target private network addresses",
|
|
1238
|
-
});
|
|
1315
|
+
function oauthRequestMayFollowRedirect(init: RequestInit): boolean {
|
|
1316
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
1317
|
+
if ((method !== "GET" && method !== "HEAD") || init.body != null) {
|
|
1318
|
+
return false;
|
|
1239
1319
|
}
|
|
1320
|
+
const headers = new Headers(init.headers);
|
|
1321
|
+
return [...headers.keys()].every((name) => name === "accept");
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function cancelResponseBody(response: Response): Promise<void> {
|
|
1325
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1240
1326
|
}
|
|
1241
1327
|
|
|
1242
1328
|
function parseWwwAuthenticate(header: string | null): WwwAuthenticateChallenge {
|