@opengeni/network 0.2.3 → 0.3.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +396 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-oauth-discovery.d.ts +79 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/mcp-oauth-discovery.ts +563 -0
package/README.md
CHANGED
|
@@ -41,3 +41,29 @@ The package also exports `readJsonBase64Field` for provider APIs that return
|
|
|
41
41
|
large binary artifacts inside JSON. It validates declared and streamed limits,
|
|
42
42
|
decodes canonical base64 incrementally, and avoids retaining the JSON envelope
|
|
43
43
|
or encoded string. Callers remain responsible for validating the decoded media.
|
|
44
|
+
|
|
45
|
+
## MCP OAuth discovery
|
|
46
|
+
|
|
47
|
+
`resolveMcpOAuthDiscovery` provides the shared MCP OAuth discovery state machine
|
|
48
|
+
used by runtime connections and catalog diagnostics. It prefers RFC 9728
|
|
49
|
+
Protected Resource Metadata and permits the MCP 2025-03-26 compatibility path
|
|
50
|
+
only after every PRM candidate is explicitly absent with HTTP 404 or 410. The
|
|
51
|
+
legacy path requires a Bearer/OAuth challenge, RFC 8414 metadata on the MCP
|
|
52
|
+
origin, an exact issuer match, same-origin resource binding, and PKCE S256.
|
|
53
|
+
|
|
54
|
+
The resolver is transport-independent. Its `fetchMetadata` callback must
|
|
55
|
+
validate each candidate before I/O, bound response size and duration, disable
|
|
56
|
+
automatic redirects, and independently validate and DNS-pin every redirect
|
|
57
|
+
hop. It must return `status: "absent"` only for HTTP 404 or 410 and throw for
|
|
58
|
+
network, redirect, destination-policy, HTTP, body, and JSON failures; otherwise
|
|
59
|
+
an unsafe or unreachable PRM endpoint could be mistaken for legacy absence.
|
|
60
|
+
`validateEndpoint` and `canonicalizeResource` provide the caller's deployment
|
|
61
|
+
policy and canonical identifier rules.
|
|
62
|
+
|
|
63
|
+
Successful results include the discovery mode, normalized metadata, structured
|
|
64
|
+
classification, and a stable SHA-256 provenance digest. Fail-closed errors carry
|
|
65
|
+
classifications for broken discovery, unverified legacy defaults, or a legacy
|
|
66
|
+
cross-origin configuration that requires an explicit reviewed profile. These
|
|
67
|
+
classifications are diagnostics; callers still decide whether a provider
|
|
68
|
+
profile is supported and must perform fresh runtime discovery before granting
|
|
69
|
+
credentials.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -259,6 +259,396 @@ function isJsonWhitespace(byte) {
|
|
|
259
259
|
return byte === 32 || byte === 9 || byte === 10 || byte === 13;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// src/mcp-oauth-discovery.ts
|
|
263
|
+
import { createHash } from "crypto";
|
|
264
|
+
var McpOAuthDiscoveryError = class extends Error {
|
|
265
|
+
constructor(stage, classification, message, cause) {
|
|
266
|
+
super(message);
|
|
267
|
+
this.stage = stage;
|
|
268
|
+
this.classification = classification;
|
|
269
|
+
this.cause = cause;
|
|
270
|
+
this.name = "McpOAuthDiscoveryError";
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
async function resolveMcpOAuthDiscovery(input) {
|
|
274
|
+
const prmCandidates = protectedResourceMetadataCandidates(
|
|
275
|
+
input.resourceUrl,
|
|
276
|
+
input.challenge.resourceMetadata
|
|
277
|
+
);
|
|
278
|
+
let prmDocument = null;
|
|
279
|
+
for (const candidate of prmCandidates) {
|
|
280
|
+
const fetched = await input.fetchMetadata({ kind: "protected_resource", url: candidate });
|
|
281
|
+
if (fetched.status === "absent") continue;
|
|
282
|
+
prmDocument = fetched;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
if (prmDocument) {
|
|
286
|
+
const prm = parseProtectedResourceMetadata(prmDocument, input);
|
|
287
|
+
const authorizationServer = prm.authorizationServers[0];
|
|
288
|
+
const as2 = await discoverAuthorizationServerMetadata(authorizationServer, "modern", input);
|
|
289
|
+
return discoveryResult({
|
|
290
|
+
mode: "rfc9728_protected_resource",
|
|
291
|
+
classification: "oauth_rfc9728",
|
|
292
|
+
challenge: input.challenge,
|
|
293
|
+
resource: prm.resource ?? input.canonicalizeResource(input.resourceUrl),
|
|
294
|
+
prm,
|
|
295
|
+
as: as2
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (input.challenge.resourceMetadata !== void 0) {
|
|
299
|
+
throw new McpOAuthDiscoveryError(
|
|
300
|
+
"protected_resource_metadata",
|
|
301
|
+
"oauth_discovery_broken",
|
|
302
|
+
"MCP advertised protected resource metadata, but no metadata document was found"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (!input.challenge.scheme) {
|
|
306
|
+
throw new McpOAuthDiscoveryError(
|
|
307
|
+
"protected_resource_metadata",
|
|
308
|
+
"oauth_discovery_broken",
|
|
309
|
+
"MCP protected resource metadata was absent and the server returned no Bearer/OAuth challenge"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const resource = input.canonicalizeResource(input.resourceUrl);
|
|
313
|
+
const resourceOrigin = new URL(resource).origin;
|
|
314
|
+
const as = await discoverAuthorizationServerMetadata(resourceOrigin, "legacy", input);
|
|
315
|
+
if (new URL(as.issuer).origin !== resourceOrigin) {
|
|
316
|
+
throw new McpOAuthDiscoveryError(
|
|
317
|
+
"authorization_server_metadata",
|
|
318
|
+
"oauth_requires_profile",
|
|
319
|
+
"legacy MCP OAuth discovery requires the authorization server issuer to share the MCP server origin"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const syntheticPrm = {
|
|
323
|
+
resource,
|
|
324
|
+
authorizationServers: [as.issuer],
|
|
325
|
+
scopesSupported: [...input.challenge.scope],
|
|
326
|
+
raw: {
|
|
327
|
+
resource,
|
|
328
|
+
authorization_servers: [as.issuer],
|
|
329
|
+
scopes_supported: [...input.challenge.scope]
|
|
330
|
+
},
|
|
331
|
+
metadataUrl: ""
|
|
332
|
+
};
|
|
333
|
+
return discoveryResult({
|
|
334
|
+
mode: "legacy_2025_03_26_metadata",
|
|
335
|
+
classification: "oauth_legacy_same_origin_metadata",
|
|
336
|
+
challenge: input.challenge,
|
|
337
|
+
resource,
|
|
338
|
+
prm: syntheticPrm,
|
|
339
|
+
as
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function parseMcpOAuthChallenge(header) {
|
|
343
|
+
if (!header) return { scheme: null, scope: [] };
|
|
344
|
+
const challenges = parseAuthenticateChallenges(header);
|
|
345
|
+
if (!challenges) return { scheme: null, scope: [] };
|
|
346
|
+
const oauthChallenges = challenges.filter((candidate) => {
|
|
347
|
+
const scheme = candidate.scheme.toLowerCase();
|
|
348
|
+
return scheme === "bearer" || scheme === "oauth";
|
|
349
|
+
}).map(parseOAuthChallenge);
|
|
350
|
+
return oauthChallenges.find((challenge) => challenge.resourceMetadata !== void 0) ?? oauthChallenges[0] ?? { scheme: null, scope: [] };
|
|
351
|
+
}
|
|
352
|
+
function parseOAuthChallenge(challenge) {
|
|
353
|
+
const scheme = challenge.scheme.toLowerCase();
|
|
354
|
+
const resourceMetadataPresent = challenge.parameterParts.some(
|
|
355
|
+
(part) => /^resource_metadata\s*=/i.test(part.trim())
|
|
356
|
+
);
|
|
357
|
+
const paramsText = challenge.parameterParts.join(",");
|
|
358
|
+
const params = {};
|
|
359
|
+
const re = /([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*("(?:[^"\\]|\\.)*"|[^,\s]+)/g;
|
|
360
|
+
let match;
|
|
361
|
+
while ((match = re.exec(paramsText)) !== null) {
|
|
362
|
+
const raw = match[2];
|
|
363
|
+
params[match[1].toLowerCase()] = raw.startsWith('"') ? raw.slice(1, -1).replace(/\\"/g, '"') : raw;
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
scheme,
|
|
367
|
+
scope: params.scope ? params.scope.split(/\s+/).filter(Boolean) : [],
|
|
368
|
+
...resourceMetadataPresent ? { resourceMetadata: params.resource_metadata ?? "" } : {},
|
|
369
|
+
...params.error ? { error: params.error } : {}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
var MAX_EMPTY_AUTHENTICATE_LIST_ELEMENTS = 8;
|
|
373
|
+
function parseAuthenticateChallenges(header) {
|
|
374
|
+
const segments = splitAuthenticateHeader(header);
|
|
375
|
+
if (!segments) return null;
|
|
376
|
+
const challenges = [];
|
|
377
|
+
let emptyElements = 0;
|
|
378
|
+
for (const segment of segments) {
|
|
379
|
+
const trimmed = segment.trim();
|
|
380
|
+
if (!trimmed) {
|
|
381
|
+
emptyElements += 1;
|
|
382
|
+
if (emptyElements > MAX_EMPTY_AUTHENTICATE_LIST_ELEMENTS) return null;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const token = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+/.exec(trimmed)?.[0];
|
|
386
|
+
if (!token) return null;
|
|
387
|
+
let cursor = token.length;
|
|
388
|
+
while (/\s/.test(trimmed[cursor] ?? "")) cursor += 1;
|
|
389
|
+
if (trimmed[cursor] === "=") {
|
|
390
|
+
const current = challenges.at(-1);
|
|
391
|
+
if (!current) return null;
|
|
392
|
+
current.parameterParts.push(trimmed);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const remainder = trimmed.slice(token.length);
|
|
396
|
+
if (remainder && !/^\s/.test(remainder)) return null;
|
|
397
|
+
challenges.push({
|
|
398
|
+
scheme: token,
|
|
399
|
+
parameterParts: remainder.trim() ? [remainder.trim()] : []
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return challenges;
|
|
403
|
+
}
|
|
404
|
+
function splitAuthenticateHeader(header) {
|
|
405
|
+
const segments = [];
|
|
406
|
+
let segmentStart = 0;
|
|
407
|
+
let quoted = false;
|
|
408
|
+
let escaped = false;
|
|
409
|
+
for (let index = 0; index < header.length; index += 1) {
|
|
410
|
+
const character = header[index];
|
|
411
|
+
if (quoted) {
|
|
412
|
+
if (escaped) {
|
|
413
|
+
escaped = false;
|
|
414
|
+
} else if (character === "\\") {
|
|
415
|
+
escaped = true;
|
|
416
|
+
} else if (character === '"') {
|
|
417
|
+
quoted = false;
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (character === '"') {
|
|
422
|
+
quoted = true;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (character === ",") {
|
|
426
|
+
segments.push(header.slice(segmentStart, index));
|
|
427
|
+
segmentStart = index + 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (quoted) return null;
|
|
431
|
+
segments.push(header.slice(segmentStart));
|
|
432
|
+
return segments;
|
|
433
|
+
}
|
|
434
|
+
function protectedResourceMetadataCandidates(resourceUrl, advertisedUrl) {
|
|
435
|
+
return uniqueStrings([
|
|
436
|
+
...advertisedUrl !== void 0 ? [advertisedUrl] : [],
|
|
437
|
+
...oauthWellKnownCandidates(resourceUrl, "oauth-protected-resource")
|
|
438
|
+
]);
|
|
439
|
+
}
|
|
440
|
+
function authorizationServerMetadataCandidates(authorizationServer) {
|
|
441
|
+
return uniqueStrings([
|
|
442
|
+
...oauthWellKnownCandidates(authorizationServer, "oauth-authorization-server"),
|
|
443
|
+
...oauthWellKnownCandidates(authorizationServer, "openid-configuration"),
|
|
444
|
+
authorizationServer
|
|
445
|
+
]);
|
|
446
|
+
}
|
|
447
|
+
function legacyAuthorizationServerMetadataCandidates(resourceUrl) {
|
|
448
|
+
const origin = new URL(resourceUrl).origin;
|
|
449
|
+
return [`${origin}/.well-known/oauth-authorization-server`];
|
|
450
|
+
}
|
|
451
|
+
function oauthWellKnownCandidates(rawUrl, name) {
|
|
452
|
+
const url = new URL(rawUrl);
|
|
453
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
454
|
+
return uniqueStrings([
|
|
455
|
+
`${url.origin}/.well-known/${name}${path ? `/${path}` : ""}`,
|
|
456
|
+
`${url.origin}${path ? `/${path}` : ""}/.well-known/${name}`,
|
|
457
|
+
`${url.origin}/.well-known/${name}`
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
function parseProtectedResourceMetadata(fetched, input) {
|
|
461
|
+
const authorizationServers = stringArray(fetched.document.authorization_servers).map(
|
|
462
|
+
(value) => validateDiscoveryEndpoint(
|
|
463
|
+
input,
|
|
464
|
+
value,
|
|
465
|
+
"OAuth authorization server",
|
|
466
|
+
"protected_resource_metadata"
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
if (authorizationServers.length === 0) {
|
|
470
|
+
throw new McpOAuthDiscoveryError(
|
|
471
|
+
"protected_resource_metadata",
|
|
472
|
+
"oauth_discovery_broken",
|
|
473
|
+
"MCP protected resource metadata did not advertise an authorization server"
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const resourceValue = stringValue(fetched.document.resource);
|
|
477
|
+
return {
|
|
478
|
+
authorizationServers,
|
|
479
|
+
scopesSupported: stringArray(fetched.document.scopes_supported),
|
|
480
|
+
raw: fetched.document,
|
|
481
|
+
metadataUrl: fetched.url,
|
|
482
|
+
...resourceValue ? { resource: input.canonicalizeResource(resourceValue) } : {}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function discoverAuthorizationServerMetadata(authorizationServer, profile, input) {
|
|
486
|
+
const safeAuthorizationServer = validateDiscoveryEndpoint(
|
|
487
|
+
input,
|
|
488
|
+
authorizationServer,
|
|
489
|
+
"OAuth authorization server",
|
|
490
|
+
"authorization_server_metadata"
|
|
491
|
+
).replace(/\/+$/, "");
|
|
492
|
+
const candidates = profile === "legacy" ? legacyAuthorizationServerMetadataCandidates(safeAuthorizationServer) : authorizationServerMetadataCandidates(safeAuthorizationServer);
|
|
493
|
+
let fetched = null;
|
|
494
|
+
for (const candidate of candidates) {
|
|
495
|
+
const result = await input.fetchMetadata({ kind: "authorization_server", url: candidate });
|
|
496
|
+
if (result.status === "absent") continue;
|
|
497
|
+
fetched = result;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (!fetched) {
|
|
501
|
+
throw new McpOAuthDiscoveryError(
|
|
502
|
+
"authorization_server_metadata",
|
|
503
|
+
profile === "legacy" ? "oauth_legacy_default_endpoints_unverified" : "oauth_discovery_broken",
|
|
504
|
+
profile === "legacy" ? "legacy MCP OAuth metadata was absent; default authorization endpoints require explicit verification" : "could not discover OAuth authorization server metadata"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const authorizationEndpoint = requiredString(
|
|
508
|
+
fetched.document.authorization_endpoint,
|
|
509
|
+
"authorization_endpoint",
|
|
510
|
+
"authorization_server_metadata"
|
|
511
|
+
);
|
|
512
|
+
const tokenEndpoint = requiredString(
|
|
513
|
+
fetched.document.token_endpoint,
|
|
514
|
+
"token_endpoint",
|
|
515
|
+
"authorization_server_metadata"
|
|
516
|
+
);
|
|
517
|
+
const issuerValue = stringValue(fetched.document.issuer);
|
|
518
|
+
if (profile === "legacy" && !issuerValue) {
|
|
519
|
+
throw new McpOAuthDiscoveryError(
|
|
520
|
+
"authorization_server_metadata",
|
|
521
|
+
"oauth_discovery_broken",
|
|
522
|
+
"legacy MCP OAuth metadata did not include issuer"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const issuer = validateDiscoveryEndpoint(
|
|
526
|
+
input,
|
|
527
|
+
issuerValue ?? safeAuthorizationServer,
|
|
528
|
+
"OAuth issuer",
|
|
529
|
+
"authorization_server_metadata"
|
|
530
|
+
);
|
|
531
|
+
if (profile === "legacy" && normalizedIssuerIdentifier(issuer) !== normalizedIssuerIdentifier(safeAuthorizationServer)) {
|
|
532
|
+
const crossOrigin = new URL(issuer).origin !== new URL(safeAuthorizationServer).origin;
|
|
533
|
+
throw new McpOAuthDiscoveryError(
|
|
534
|
+
"authorization_server_metadata",
|
|
535
|
+
crossOrigin ? "oauth_requires_profile" : "oauth_discovery_broken",
|
|
536
|
+
"OAuth authorization server metadata issuer did not match the selected authorization server"
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const registrationEndpoint = stringValue(fetched.document.registration_endpoint);
|
|
540
|
+
const parsed = {
|
|
541
|
+
issuer,
|
|
542
|
+
authorizationServer: safeAuthorizationServer,
|
|
543
|
+
authorizationEndpoint: validateDiscoveryEndpoint(
|
|
544
|
+
input,
|
|
545
|
+
authorizationEndpoint,
|
|
546
|
+
"OAuth authorization endpoint",
|
|
547
|
+
"authorization_server_metadata"
|
|
548
|
+
),
|
|
549
|
+
tokenEndpoint: validateDiscoveryEndpoint(
|
|
550
|
+
input,
|
|
551
|
+
tokenEndpoint,
|
|
552
|
+
"OAuth token endpoint",
|
|
553
|
+
"authorization_server_metadata"
|
|
554
|
+
),
|
|
555
|
+
clientIdMetadataDocumentSupported: fetched.document.client_id_metadata_document_supported === true,
|
|
556
|
+
tokenEndpointAuthMethodsSupported: stringArray(
|
|
557
|
+
fetched.document.token_endpoint_auth_methods_supported
|
|
558
|
+
),
|
|
559
|
+
codeChallengeMethodsSupported: stringArray(fetched.document.code_challenge_methods_supported),
|
|
560
|
+
raw: fetched.document,
|
|
561
|
+
metadataUrl: fetched.url,
|
|
562
|
+
...registrationEndpoint ? {
|
|
563
|
+
registrationEndpoint: validateDiscoveryEndpoint(
|
|
564
|
+
input,
|
|
565
|
+
registrationEndpoint,
|
|
566
|
+
"OAuth registration endpoint",
|
|
567
|
+
"authorization_server_metadata"
|
|
568
|
+
)
|
|
569
|
+
} : {}
|
|
570
|
+
};
|
|
571
|
+
if (!parsed.codeChallengeMethodsSupported.includes("S256")) {
|
|
572
|
+
throw new McpOAuthDiscoveryError(
|
|
573
|
+
"authorization_server_metadata",
|
|
574
|
+
"oauth_discovery_broken",
|
|
575
|
+
"authorization server does not support required PKCE S256"
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return parsed;
|
|
579
|
+
}
|
|
580
|
+
function discoveryResult(input) {
|
|
581
|
+
const protectedResourceMetadataUrl = input.prm.metadataUrl || null;
|
|
582
|
+
return {
|
|
583
|
+
mode: input.mode,
|
|
584
|
+
classification: input.classification,
|
|
585
|
+
challenge: input.challenge,
|
|
586
|
+
resource: input.resource,
|
|
587
|
+
protectedResourceMetadata: input.prm,
|
|
588
|
+
authorizationServerMetadata: input.as,
|
|
589
|
+
provenance: {
|
|
590
|
+
protectedResourceMetadataUrl,
|
|
591
|
+
authorizationServerMetadataUrl: input.as.metadataUrl,
|
|
592
|
+
metadataSha256: createHash("sha256").update(
|
|
593
|
+
stableJson({
|
|
594
|
+
mode: input.mode,
|
|
595
|
+
resource: input.resource,
|
|
596
|
+
challenge: input.challenge,
|
|
597
|
+
protectedResourceMetadata: {
|
|
598
|
+
url: protectedResourceMetadataUrl,
|
|
599
|
+
document: input.prm.raw
|
|
600
|
+
},
|
|
601
|
+
authorizationServerMetadata: {
|
|
602
|
+
url: input.as.metadataUrl,
|
|
603
|
+
document: input.as.raw
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
).digest("hex")
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function validateDiscoveryEndpoint(input, rawUrl, label, stage) {
|
|
611
|
+
try {
|
|
612
|
+
return input.validateEndpoint(rawUrl, label);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
throw new McpOAuthDiscoveryError(
|
|
615
|
+
stage,
|
|
616
|
+
"oauth_discovery_broken",
|
|
617
|
+
error instanceof Error ? error.message : `${label} was invalid`,
|
|
618
|
+
error
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function normalizedIssuerIdentifier(value) {
|
|
623
|
+
return value.replace(/\/+$/, "");
|
|
624
|
+
}
|
|
625
|
+
function stableJson(value) {
|
|
626
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
627
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
628
|
+
const record = value;
|
|
629
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
630
|
+
}
|
|
631
|
+
function uniqueStrings(values) {
|
|
632
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
633
|
+
}
|
|
634
|
+
function stringArray(value) {
|
|
635
|
+
return Array.isArray(value) ? uniqueStrings(value.filter((entry) => typeof entry === "string")) : [];
|
|
636
|
+
}
|
|
637
|
+
function stringValue(value) {
|
|
638
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
639
|
+
}
|
|
640
|
+
function requiredString(value, field, stage) {
|
|
641
|
+
const parsed = stringValue(value);
|
|
642
|
+
if (!parsed) {
|
|
643
|
+
throw new McpOAuthDiscoveryError(
|
|
644
|
+
stage,
|
|
645
|
+
"oauth_discovery_broken",
|
|
646
|
+
`OAuth metadata did not include ${field}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return parsed;
|
|
650
|
+
}
|
|
651
|
+
|
|
262
652
|
// src/index.ts
|
|
263
653
|
var DISPATCHER_CLOSE_TIMEOUT_MS = 100;
|
|
264
654
|
var OAUTH_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
@@ -876,18 +1266,24 @@ function ipv6Constant(address) {
|
|
|
876
1266
|
export {
|
|
877
1267
|
DestinationPolicyError,
|
|
878
1268
|
JsonBase64ResponseError,
|
|
1269
|
+
McpOAuthDiscoveryError,
|
|
879
1270
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
880
1271
|
RequestDeadlineError,
|
|
881
1272
|
ResponseBodyLimitError,
|
|
1273
|
+
authorizationServerMetadataCandidates,
|
|
882
1274
|
isInvalidAddress,
|
|
883
1275
|
isLocalTestEnvironment,
|
|
884
1276
|
isNonPublicAddress,
|
|
885
1277
|
isPrivateAddress,
|
|
1278
|
+
legacyAuthorizationServerMetadataCandidates,
|
|
1279
|
+
parseMcpOAuthChallenge,
|
|
886
1280
|
pinnedFetch,
|
|
1281
|
+
protectedResourceMetadataCandidates,
|
|
887
1282
|
readJsonBase64Field,
|
|
888
1283
|
readResponseBodyBounded,
|
|
889
1284
|
readResponseJsonBounded,
|
|
890
1285
|
readResponseTextBounded,
|
|
1286
|
+
resolveMcpOAuthDiscovery,
|
|
891
1287
|
resolvePinnedDestination,
|
|
892
1288
|
undiciFetch,
|
|
893
1289
|
validateHttpUrl
|