@stigmer/outbound 3.18.1-dev.20260919070736
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/LICENSE +190 -0
- package/README.md +22 -0
- package/egress/address.d.ts +23 -0
- package/egress/address.d.ts.map +1 -0
- package/egress/address.js +148 -0
- package/egress/address.js.map +1 -0
- package/egress/check.d.ts +50 -0
- package/egress/check.d.ts.map +1 -0
- package/egress/check.js +127 -0
- package/egress/check.js.map +1 -0
- package/egress/fetch.d.ts +52 -0
- package/egress/fetch.d.ts.map +1 -0
- package/egress/fetch.js +59 -0
- package/egress/fetch.js.map +1 -0
- package/egress/index.d.ts +10 -0
- package/egress/index.d.ts.map +1 -0
- package/egress/index.js +10 -0
- package/egress/index.js.map +1 -0
- package/egress/node-lookup.d.ts +4 -0
- package/egress/node-lookup.d.ts.map +1 -0
- package/egress/node-lookup.js +20 -0
- package/egress/node-lookup.js.map +1 -0
- package/index.d.ts +11 -0
- package/index.d.ts.map +1 -0
- package/index.js +11 -0
- package/index.js.map +1 -0
- package/mcp-oauth/challenge.d.ts +25 -0
- package/mcp-oauth/challenge.d.ts.map +1 -0
- package/mcp-oauth/challenge.js +33 -0
- package/mcp-oauth/challenge.js.map +1 -0
- package/mcp-oauth/index.d.ts +11 -0
- package/mcp-oauth/index.d.ts.map +1 -0
- package/mcp-oauth/index.js +11 -0
- package/mcp-oauth/index.js.map +1 -0
- package/mcp-oauth/metadata.d.ts +86 -0
- package/mcp-oauth/metadata.d.ts.map +1 -0
- package/mcp-oauth/metadata.js +113 -0
- package/mcp-oauth/metadata.js.map +1 -0
- package/mcp-oauth/probe.d.ts +68 -0
- package/mcp-oauth/probe.d.ts.map +1 -0
- package/mcp-oauth/probe.js +35 -0
- package/mcp-oauth/probe.js.map +1 -0
- package/mcp-oauth/request.d.ts +44 -0
- package/mcp-oauth/request.d.ts.map +1 -0
- package/mcp-oauth/request.js +71 -0
- package/mcp-oauth/request.js.map +1 -0
- package/package.json +42 -0
- package/src/__tests__/address.test.ts +88 -0
- package/src/__tests__/challenge.test.ts +41 -0
- package/src/__tests__/check.test.ts +98 -0
- package/src/__tests__/fetch.test.ts +161 -0
- package/src/__tests__/metadata.test.ts +171 -0
- package/src/__tests__/probe.test.ts +98 -0
- package/src/egress/address.ts +168 -0
- package/src/egress/check.ts +163 -0
- package/src/egress/fetch.ts +104 -0
- package/src/egress/index.ts +17 -0
- package/src/egress/node-lookup.ts +22 -0
- package/src/index.ts +10 -0
- package/src/mcp-oauth/challenge.ts +33 -0
- package/src/mcp-oauth/index.ts +25 -0
- package/src/mcp-oauth/metadata.ts +185 -0
- package/src/mcp-oauth/probe.ts +91 -0
- package/src/mcp-oauth/request.ts +81 -0
package/egress/fetch.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { checkEgress, EgressError } from "./check.js";
|
|
2
|
+
/** The default hop budget: a scheme upgrade and a canonicalisation, with one to spare. */
|
|
3
|
+
export const DEFAULT_MAX_REDIRECTS = 3;
|
|
4
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
5
|
+
/**
|
|
6
|
+
* An `OutboundFetch` in the global `fetch`'s shape, for a seam typed
|
|
7
|
+
* `typeof fetch`. A `Request` input is refused rather than flattened: this
|
|
8
|
+
* library's callers pass URLs, and a wrapper that silently dropped a
|
|
9
|
+
* Request's body or headers would be a bug that looked like a network
|
|
10
|
+
* failure.
|
|
11
|
+
*/
|
|
12
|
+
export function asFetch(outbound) {
|
|
13
|
+
return (input, init) => {
|
|
14
|
+
if (typeof input === "string" || input instanceof URL)
|
|
15
|
+
return outbound(input, init);
|
|
16
|
+
return Promise.reject(new TypeError("OutboundFetch takes a URL, not a Request"));
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Compose a fetch that judges every URL and every hop under `policy`. */
|
|
20
|
+
export function guardedFetch(policy, options) {
|
|
21
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
22
|
+
return async (input, init) => {
|
|
23
|
+
let url = new URL(input);
|
|
24
|
+
let method = (init?.method ?? "GET").toUpperCase();
|
|
25
|
+
let body = init?.body;
|
|
26
|
+
const headers = new Headers(init?.headers);
|
|
27
|
+
const callerRedirect = init?.redirect ?? "follow";
|
|
28
|
+
for (let hop = 0;; hop += 1) {
|
|
29
|
+
const check = await checkEgress(url, policy, { lookup: options.lookup, signal: init?.signal });
|
|
30
|
+
if (!check.ok)
|
|
31
|
+
throw new EgressError(check.refusal);
|
|
32
|
+
const response = await options.fetchImpl(url, { ...init, method, body, headers, redirect: "manual" });
|
|
33
|
+
const location = response.headers.get("location");
|
|
34
|
+
if (!REDIRECT_STATUSES.has(response.status) || location === null || callerRedirect === "manual") {
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
if (callerRedirect === "error") {
|
|
38
|
+
await response.body?.cancel();
|
|
39
|
+
throw new TypeError(`fetch refused to follow a redirect from ${url.href}`);
|
|
40
|
+
}
|
|
41
|
+
if (hop >= maxRedirects) {
|
|
42
|
+
await response.body?.cancel();
|
|
43
|
+
throw new EgressError({ kind: "too-many-redirects", url, hops: maxRedirects });
|
|
44
|
+
}
|
|
45
|
+
await response.body?.cancel();
|
|
46
|
+
const next = new URL(location, url);
|
|
47
|
+
if (next.origin !== url.origin)
|
|
48
|
+
headers.delete("authorization");
|
|
49
|
+
if (response.status === 303 || ((response.status === 301 || response.status === 302) && method !== "GET" && method !== "HEAD")) {
|
|
50
|
+
method = "GET";
|
|
51
|
+
body = undefined;
|
|
52
|
+
headers.delete("content-type");
|
|
53
|
+
headers.delete("content-length");
|
|
54
|
+
}
|
|
55
|
+
url = next;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/egress/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound/egress`: the address policy a Stigmer process dials
|
|
3
|
+
* user-supplied URLs under, the check that applies it to a URL, and the
|
|
4
|
+
* fetch that applies it to every hop. See each module's header.
|
|
5
|
+
*/
|
|
6
|
+
export { blockedReason, egressPolicyForPosture, type EgressPolicy, type EgressPosture } from "./address.js";
|
|
7
|
+
export { checkEgress, describeRefusal, EgressError, type EgressCheck, type EgressCheckOptions, type EgressRefusal, type LookupFn, } from "./check.js";
|
|
8
|
+
export { asFetch, DEFAULT_MAX_REDIRECTS, guardedFetch, type GuardedFetchOptions, type OutboundFetch } from "./fetch.js";
|
|
9
|
+
export { nodeLookup } from "./node-lookup.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAC5G,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACxH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
package/egress/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound/egress`: the address policy a Stigmer process dials
|
|
3
|
+
* user-supplied URLs under, the check that applies it to a URL, and the
|
|
4
|
+
* fetch that applies it to every hop. See each module's header.
|
|
5
|
+
*/
|
|
6
|
+
export { blockedReason, egressPolicyForPosture } from "./address.js";
|
|
7
|
+
export { checkEgress, describeRefusal, EgressError, } from "./check.js";
|
|
8
|
+
export { asFetch, DEFAULT_MAX_REDIRECTS, guardedFetch } from "./fetch.js";
|
|
9
|
+
export { nodeLookup } from "./node-lookup.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAyC,MAAM,cAAc,CAAC;AAC5G,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,GAKZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAgD,MAAM,YAAY,CAAC;AACxH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-lookup.d.ts","sourceRoot":"","sources":["../../src/egress/node-lookup.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,+EAA+E;AAC/E,wBAAgB,UAAU,IAAI,QAAQ,CAKrC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one Node-bound piece of the egress module: a `LookupFn` over
|
|
3
|
+
* `node:dns/promises`. Kept in its own file so the judgement (`check.ts`,
|
|
4
|
+
* `address.ts`) stays free of I/O and a test composes the check with a
|
|
5
|
+
* table instead of a resolver.
|
|
6
|
+
*
|
|
7
|
+
* `all: true` because the check must judge every address a name has;
|
|
8
|
+
* `verbatim: true` so the resolver's order is kept rather than re-sorted,
|
|
9
|
+
* which keeps the refusal's named address the one a socket would have
|
|
10
|
+
* taken first.
|
|
11
|
+
*/
|
|
12
|
+
import { lookup } from "node:dns/promises";
|
|
13
|
+
/** Resolve through the process's resolver, every record, in resolver order. */
|
|
14
|
+
export function nodeLookup() {
|
|
15
|
+
return async (hostname) => {
|
|
16
|
+
const records = await lookup(hostname, { all: true, verbatim: true });
|
|
17
|
+
return records.map((record) => record.address);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=node-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-lookup.js","sourceRoot":"","sources":["../../src/egress/node-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAI3C,+EAA+E;AAC/E,MAAM,UAAU,UAAU;IACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC,CAAC;AACJ,CAAC"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound`: the rules a Stigmer process dials a user-supplied
|
|
3
|
+
* endpoint by. Two entry points, also reachable as subpaths:
|
|
4
|
+
* `./egress` (the address policy, its check, the guarded fetch) and
|
|
5
|
+
* `./mcp-oauth` (reading an MCP endpoint's authentication and finding its
|
|
6
|
+
* login server). Consumers: the control plane, the runner, the catalogue
|
|
7
|
+
* audit. The README says why one library and not three copies.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./egress/index.js";
|
|
10
|
+
export * from "./mcp-oauth/index.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC"}
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound`: the rules a Stigmer process dials a user-supplied
|
|
3
|
+
* endpoint by. Two entry points, also reachable as subpaths:
|
|
4
|
+
* `./egress` (the address policy, its check, the guarded fetch) and
|
|
5
|
+
* `./mcp-oauth` (reading an MCP endpoint's authentication and finding its
|
|
6
|
+
* login server). Consumers: the control plane, the runner, the catalogue
|
|
7
|
+
* audit. The README says why one library and not three copies.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./egress/index.js";
|
|
10
|
+
export * from "./mcp-oauth/index.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OAuth-challenge rule: what a `WWW-Authenticate` header must say for
|
|
3
|
+
* Stigmer to tell a user "this server requires OAuth".
|
|
4
|
+
*
|
|
5
|
+
* One home for a rule three processes apply. The runner applies it at the
|
|
6
|
+
* first failed tool call (shared/mcp-oauth-detect.ts, where the rule was
|
|
7
|
+
* born); the control plane applies it at save time so a URL-only server
|
|
8
|
+
* gets its Sign in button before anyone runs it; the catalogue audit applies
|
|
9
|
+
* it to decide what the official catalogue may promise. A rule with one copy
|
|
10
|
+
* per process would let the storefront call OAuth what the runner calls a
|
|
11
|
+
* bad token.
|
|
12
|
+
*
|
|
13
|
+
* The rule, per the MCP Authorization specification and RFC 9728: the
|
|
14
|
+
* scheme is `Bearer`, and the challenge names either an OAuth realm or a
|
|
15
|
+
* `resource_metadata` pointer. A plain `Bearer` 401 (an invalid static API
|
|
16
|
+
* key) is not an OAuth requirement and must not be read as one, or a user
|
|
17
|
+
* with a mistyped token would be sent to a Sign in that does not exist.
|
|
18
|
+
*
|
|
19
|
+
* Proven by __tests__/challenge.test.ts, the runner's cases moved here.
|
|
20
|
+
*/
|
|
21
|
+
/** Whether a `WWW-Authenticate` value is an OAuth challenge. */
|
|
22
|
+
export declare function isOAuthChallenge(wwwAuthenticate: string): boolean;
|
|
23
|
+
/** The `resource_metadata` URL a challenge points at (RFC 9728 section 5.1), if any. */
|
|
24
|
+
export declare function parseResourceMetadataUrl(wwwAuthenticate: string): string | undefined;
|
|
25
|
+
//# sourceMappingURL=challenge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/challenge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED,wFAAwF;AACxF,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGpF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OAuth-challenge rule: what a `WWW-Authenticate` header must say for
|
|
3
|
+
* Stigmer to tell a user "this server requires OAuth".
|
|
4
|
+
*
|
|
5
|
+
* One home for a rule three processes apply. The runner applies it at the
|
|
6
|
+
* first failed tool call (shared/mcp-oauth-detect.ts, where the rule was
|
|
7
|
+
* born); the control plane applies it at save time so a URL-only server
|
|
8
|
+
* gets its Sign in button before anyone runs it; the catalogue audit applies
|
|
9
|
+
* it to decide what the official catalogue may promise. A rule with one copy
|
|
10
|
+
* per process would let the storefront call OAuth what the runner calls a
|
|
11
|
+
* bad token.
|
|
12
|
+
*
|
|
13
|
+
* The rule, per the MCP Authorization specification and RFC 9728: the
|
|
14
|
+
* scheme is `Bearer`, and the challenge names either an OAuth realm or a
|
|
15
|
+
* `resource_metadata` pointer. A plain `Bearer` 401 (an invalid static API
|
|
16
|
+
* key) is not an OAuth requirement and must not be read as one, or a user
|
|
17
|
+
* with a mistyped token would be sent to a Sign in that does not exist.
|
|
18
|
+
*
|
|
19
|
+
* Proven by __tests__/challenge.test.ts, the runner's cases moved here.
|
|
20
|
+
*/
|
|
21
|
+
/** Whether a `WWW-Authenticate` value is an OAuth challenge. */
|
|
22
|
+
export function isOAuthChallenge(wwwAuthenticate) {
|
|
23
|
+
const value = wwwAuthenticate.toLowerCase();
|
|
24
|
+
if (!value.includes("bearer"))
|
|
25
|
+
return false;
|
|
26
|
+
return value.includes("oauth") || value.includes("resource_metadata");
|
|
27
|
+
}
|
|
28
|
+
/** The `resource_metadata` URL a challenge points at (RFC 9728 section 5.1), if any. */
|
|
29
|
+
export function parseResourceMetadataUrl(wwwAuthenticate) {
|
|
30
|
+
const match = /resource_metadata="([^"]+)"/i.exec(wwwAuthenticate);
|
|
31
|
+
return match?.[1];
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../../src/mcp-oauth/challenge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,eAAuB;IACtD,MAAM,KAAK,GAAG,eAAe,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;AACxE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,wBAAwB,CAAC,eAAuB;IAC9D,MAAM,KAAK,GAAG,8BAA8B,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACnE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound/mcp-oauth`: how an MCP endpoint's authentication is
|
|
3
|
+
* read without a credential (the challenge rule, the complete `initialize`,
|
|
4
|
+
* the one-request probe) and how its login server is found (the RFC 9728,
|
|
5
|
+
* RFC 8414 and OpenID walk). See each module's header.
|
|
6
|
+
*/
|
|
7
|
+
export { isOAuthChallenge, parseResourceMetadataUrl } from "./challenge.js";
|
|
8
|
+
export { authorizationServerMetadataUrls, protectedResourceMetadataUrls, readAuthorizationServerMetadata, resolveAuthorizationServers, type AuthorizationServerMetadata, type MetadataAttempt, type MetadataRead, type MetadataReadDeps, } from "./metadata.js";
|
|
9
|
+
export { probeEndpointAuth, type EndpointAuthOutcome, type EndpointProbeDeps } from "./probe.js";
|
|
10
|
+
export { initializeRequest, MCP_PROTOCOL_VERSION, MCP_SESSION_HEADER, toolsListRequest, type JsonRpcRequestInit, } from "./request.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,+BAA+B,EAC/B,6BAA6B,EAC7B,+BAA+B,EAC/B,2BAA2B,EAC3B,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stigmer/outbound/mcp-oauth`: how an MCP endpoint's authentication is
|
|
3
|
+
* read without a credential (the challenge rule, the complete `initialize`,
|
|
4
|
+
* the one-request probe) and how its login server is found (the RFC 9728,
|
|
5
|
+
* RFC 8414 and OpenID walk). See each module's header.
|
|
6
|
+
*/
|
|
7
|
+
export { isOAuthChallenge, parseResourceMetadataUrl } from "./challenge.js";
|
|
8
|
+
export { authorizationServerMetadataUrls, protectedResourceMetadataUrls, readAuthorizationServerMetadata, resolveAuthorizationServers, } from "./metadata.js";
|
|
9
|
+
export { probeEndpointAuth } from "./probe.js";
|
|
10
|
+
export { initializeRequest, MCP_PROTOCOL_VERSION, MCP_SESSION_HEADER, toolsListRequest, } from "./request.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp-oauth/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,+BAA+B,EAC/B,6BAA6B,EAC7B,+BAA+B,EAC/B,2BAA2B,GAK5B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAoD,MAAM,YAAY,CAAC;AACjG,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding an MCP endpoint's login server: the RFC 9728, RFC 8414 and
|
|
3
|
+
* OpenID walk, as documents to try in order and the reader that tries them.
|
|
4
|
+
*
|
|
5
|
+
* Why a walk and not one URL. Of the 41 OAuth-protected MCP servers the
|
|
6
|
+
* catalogue audit resolved on 2026-09-19, 21 keep their login server on a
|
|
7
|
+
* different origin from the MCP URL, which only RFC 9728's protected
|
|
8
|
+
* resource metadata can point at, and several issuers carry a path, which
|
|
9
|
+
* RFC 8414 section 3 says to insert the well-known segment BEFORE (an
|
|
10
|
+
* issuer `https://auth.example/tenant` describes itself at
|
|
11
|
+
* `https://auth.example/.well-known/oauth-authorization-server/tenant`).
|
|
12
|
+
* A reader that only asked the MCP origin for `oauth-authorization-server`
|
|
13
|
+
* reached half of them.
|
|
14
|
+
*
|
|
15
|
+
* The order, from the most specific to the fallback that worked before:
|
|
16
|
+
*
|
|
17
|
+
* 1. The protected resource's own document: the `resource_metadata` URL
|
|
18
|
+
* a 401 challenge named, else `/.well-known/oauth-protected-resource`
|
|
19
|
+
* at the MCP origin with the MCP path appended, else bare
|
|
20
|
+
* (RFC 9728 section 3). It names `authorization_servers`.
|
|
21
|
+
* 2. For each issuer named, its authorization-server metadata: RFC 8414
|
|
22
|
+
* with the issuer's path inserted, RFC 8414 at the origin, then
|
|
23
|
+
* OpenID's `/.well-known/openid-configuration` under the path and at
|
|
24
|
+
* the origin (RFC 8414 section 5 allows the OpenID document).
|
|
25
|
+
* 3. When no protected-resource document exists, the MCP origin itself is
|
|
26
|
+
* the one issuer to try, which is exactly the reader this replaces.
|
|
27
|
+
*
|
|
28
|
+
* Every attempt is recorded (URL, status or error) so a caller can name the
|
|
29
|
+
* document it failed on. The reader validates nothing beyond the two
|
|
30
|
+
* endpoints a login needs; the control plane keeps its own pinned
|
|
31
|
+
* validation and copy on top. Pure over an injected fetch, one deadline per
|
|
32
|
+
* request.
|
|
33
|
+
*
|
|
34
|
+
* Proven by __tests__/metadata.test.ts.
|
|
35
|
+
*/
|
|
36
|
+
import type { OutboundFetch } from "../egress/fetch.js";
|
|
37
|
+
/** RFC 8414 metadata in the fields Sign in reads; snake_case on the wire, camelCase here. */
|
|
38
|
+
export interface AuthorizationServerMetadata {
|
|
39
|
+
/** The document these facts were read from. */
|
|
40
|
+
readonly metadataUrl: string;
|
|
41
|
+
readonly issuer: string;
|
|
42
|
+
readonly authorizationEndpoint: string;
|
|
43
|
+
readonly tokenEndpoint: string;
|
|
44
|
+
/** Empty when the login server does not register clients dynamically (RFC 7591). */
|
|
45
|
+
readonly registrationEndpoint: string;
|
|
46
|
+
readonly scopesSupported: readonly string[];
|
|
47
|
+
readonly codeChallengeMethodsSupported: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/** One GET the walk made and what came back. */
|
|
50
|
+
export interface MetadataAttempt {
|
|
51
|
+
readonly url: string;
|
|
52
|
+
readonly status?: number;
|
|
53
|
+
readonly error?: string;
|
|
54
|
+
/** Set when the document parsed but lacked the endpoint a login needs, so a caller can say which. */
|
|
55
|
+
readonly missing?: "authorization_endpoint" | "token_endpoint";
|
|
56
|
+
}
|
|
57
|
+
export type MetadataRead = {
|
|
58
|
+
readonly kind: "found";
|
|
59
|
+
readonly metadata: AuthorizationServerMetadata;
|
|
60
|
+
readonly attempts: readonly MetadataAttempt[];
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: "not-found";
|
|
63
|
+
readonly attempts: readonly MetadataAttempt[];
|
|
64
|
+
};
|
|
65
|
+
export interface MetadataReadDeps {
|
|
66
|
+
readonly fetchImpl: OutboundFetch;
|
|
67
|
+
/** Per request. */
|
|
68
|
+
readonly timeoutMs: number;
|
|
69
|
+
}
|
|
70
|
+
/** The RFC 9728 documents describing a resource, most specific first. */
|
|
71
|
+
export declare function protectedResourceMetadataUrls(resource: URL): readonly string[];
|
|
72
|
+
/** The documents describing an issuer, RFC 8414 (path inserted, then origin) then OpenID (path, then origin). */
|
|
73
|
+
export declare function authorizationServerMetadataUrls(issuer: URL): readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* The issuers protecting a resource, from its RFC 9728 document (the
|
|
76
|
+
* challenge's pointer when the caller has one, else the well-known
|
|
77
|
+
* documents at the resource). Empty when no document names any; the caller
|
|
78
|
+
* decides what that means (the control plane falls back to the origin).
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveAuthorizationServers(resourceUrl: string, pointer: string | undefined, deps: MetadataReadDeps): Promise<{
|
|
81
|
+
readonly issuers: readonly string[];
|
|
82
|
+
readonly attempts: readonly MetadataAttempt[];
|
|
83
|
+
}>;
|
|
84
|
+
/** The first document for `issuer` that names an authorization and a token endpoint. */
|
|
85
|
+
export declare function readAuthorizationServerMetadata(issuer: string, deps: MetadataReadDeps): Promise<MetadataRead>;
|
|
86
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,6FAA6F;AAC7F,MAAM,WAAW,2BAA2B;IAC1C,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,6BAA6B,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3D;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,gBAAgB,CAAC;CAChE;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,2BAA2B,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,GACzH;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,CAAC;AAElF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,mBAAmB;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,yEAAyE;AACzE,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,GAAG,GAAG,SAAS,MAAM,EAAE,CAI9E;AAED,iHAAiH;AACjH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,GAAG,GAAG,SAAS,MAAM,EAAE,CAS9E;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,CAAC,CAUjG;AAED,wFAAwF;AACxF,wBAAsB,+BAA+B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCnH"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** The RFC 9728 documents describing a resource, most specific first. */
|
|
2
|
+
export function protectedResourceMetadataUrls(resource) {
|
|
3
|
+
const bare = `${resource.origin}/.well-known/oauth-protected-resource`;
|
|
4
|
+
const path = resource.pathname.replace(/\/+$/, "");
|
|
5
|
+
return path === "" ? [bare] : [`${bare}${path}`, bare];
|
|
6
|
+
}
|
|
7
|
+
/** The documents describing an issuer, RFC 8414 (path inserted, then origin) then OpenID (path, then origin). */
|
|
8
|
+
export function authorizationServerMetadataUrls(issuer) {
|
|
9
|
+
const path = issuer.pathname.replace(/\/+$/, "");
|
|
10
|
+
const urls = [
|
|
11
|
+
...(path === "" ? [] : [`${issuer.origin}/.well-known/oauth-authorization-server${path}`]),
|
|
12
|
+
`${issuer.origin}/.well-known/oauth-authorization-server`,
|
|
13
|
+
...(path === "" ? [] : [`${issuer.origin}${path}/.well-known/openid-configuration`]),
|
|
14
|
+
`${issuer.origin}/.well-known/openid-configuration`,
|
|
15
|
+
];
|
|
16
|
+
return [...new Set(urls)];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The issuers protecting a resource, from its RFC 9728 document (the
|
|
20
|
+
* challenge's pointer when the caller has one, else the well-known
|
|
21
|
+
* documents at the resource). Empty when no document names any; the caller
|
|
22
|
+
* decides what that means (the control plane falls back to the origin).
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveAuthorizationServers(resourceUrl, pointer, deps) {
|
|
25
|
+
const attempts = [];
|
|
26
|
+
const candidates = pointer !== undefined ? [pointer] : protectedResourceMetadataUrls(new URL(resourceUrl));
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
const read = await readJson(candidate, deps, attempts);
|
|
29
|
+
if (read === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const issuers = readStringArray(read, "authorization_servers");
|
|
32
|
+
if (issuers.length > 0)
|
|
33
|
+
return { issuers, attempts };
|
|
34
|
+
}
|
|
35
|
+
return { issuers: [], attempts };
|
|
36
|
+
}
|
|
37
|
+
/** The first document for `issuer` that names an authorization and a token endpoint. */
|
|
38
|
+
export async function readAuthorizationServerMetadata(issuer, deps) {
|
|
39
|
+
const attempts = [];
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = new URL(issuer);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { kind: "not-found", attempts };
|
|
46
|
+
}
|
|
47
|
+
for (const metadataUrl of authorizationServerMetadataUrls(parsed)) {
|
|
48
|
+
const document = await readJson(metadataUrl, deps, attempts);
|
|
49
|
+
if (document === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
const authorizationEndpoint = readString(document, "authorization_endpoint");
|
|
52
|
+
const tokenEndpoint = readString(document, "token_endpoint");
|
|
53
|
+
if (authorizationEndpoint === "" || tokenEndpoint === "") {
|
|
54
|
+
const last = attempts.pop();
|
|
55
|
+
if (last !== undefined)
|
|
56
|
+
attempts.push({ ...last, missing: authorizationEndpoint === "" ? "authorization_endpoint" : "token_endpoint" });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
kind: "found",
|
|
61
|
+
attempts,
|
|
62
|
+
metadata: {
|
|
63
|
+
metadataUrl,
|
|
64
|
+
issuer: readString(document, "issuer"),
|
|
65
|
+
authorizationEndpoint,
|
|
66
|
+
tokenEndpoint,
|
|
67
|
+
registrationEndpoint: readString(document, "registration_endpoint"),
|
|
68
|
+
scopesSupported: readStringArray(document, "scopes_supported"),
|
|
69
|
+
codeChallengeMethodsSupported: readStringArray(document, "code_challenge_methods_supported"),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { kind: "not-found", attempts };
|
|
74
|
+
}
|
|
75
|
+
/** A GET expecting a JSON object; anything else is recorded and read as absent. */
|
|
76
|
+
async function readJson(url, deps, attempts) {
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await deps.fetchImpl(url, {
|
|
80
|
+
method: "GET",
|
|
81
|
+
headers: { Accept: "application/json" },
|
|
82
|
+
signal: AbortSignal.timeout(deps.timeoutMs),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
attempts.push({ url, error: error instanceof Error ? error.message : String(error) });
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
attempts.push({ url, status: response.status });
|
|
90
|
+
if (response.status !== 200) {
|
|
91
|
+
await response.body?.cancel().catch(() => undefined);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = await response.json();
|
|
96
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function readString(record, key) {
|
|
103
|
+
const value = record[key];
|
|
104
|
+
return typeof value === "string" ? value : "";
|
|
105
|
+
}
|
|
106
|
+
function readStringArray(record, key) {
|
|
107
|
+
const list = record[key];
|
|
108
|
+
return Array.isArray(list) ? list.filter((item) => typeof item === "string") : [];
|
|
109
|
+
}
|
|
110
|
+
function isRecord(value) {
|
|
111
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/mcp-oauth/metadata.ts"],"names":[],"mappings":"AAqEA,yEAAyE;AACzE,MAAM,UAAU,6BAA6B,CAAC,QAAa;IACzD,MAAM,IAAI,GAAG,GAAG,QAAQ,CAAC,MAAM,uCAAuC,CAAC;IACvE,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnD,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,+BAA+B,CAAC,MAAW;IACzD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG;QACX,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,0CAA0C,IAAI,EAAE,CAAC,CAAC;QAC1F,GAAG,MAAM,CAAC,MAAM,yCAAyC;QACzD,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,IAAI,mCAAmC,CAAC,CAAC;QACpF,GAAG,MAAM,CAAC,MAAM,mCAAmC;KACpD,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,WAAmB,EACnB,OAA2B,EAC3B,IAAsB;IAEtB,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3G,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACvD,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QACjC,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC;QAC/D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IACvD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,MAAM,CAAC,KAAK,UAAU,+BAA+B,CAAC,MAAc,EAAE,IAAsB;IAC1F,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;IACzC,CAAC;IACD,KAAK,MAAM,WAAW,IAAI,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;QAClE,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC7D,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS;QACrC,MAAM,qBAAqB,GAAG,UAAU,CAAC,QAAQ,EAAE,wBAAwB,CAAC,CAAC;QAC7E,MAAM,aAAa,GAAG,UAAU,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAC7D,IAAI,qBAAqB,KAAK,EAAE,IAAI,aAAa,KAAK,EAAE,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;YAC5B,IAAI,IAAI,KAAK,SAAS;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,qBAAqB,KAAK,EAAE,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;YACxI,SAAS;QACX,CAAC;QACD,OAAO;YACL,IAAI,EAAE,OAAO;YACb,QAAQ;YACR,QAAQ,EAAE;gBACR,WAAW;gBACX,MAAM,EAAE,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC;gBACtC,qBAAqB;gBACrB,aAAa;gBACb,oBAAoB,EAAE,UAAU,CAAC,QAAQ,EAAE,uBAAuB,CAAC;gBACnE,eAAe,EAAE,eAAe,CAAC,QAAQ,EAAE,kBAAkB,CAAC;gBAC9D,6BAA6B,EAAE,eAAe,CAAC,QAAQ,EAAE,kCAAkC,CAAC;aAC7F;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AACzC,CAAC;AAED,mFAAmF;AACnF,KAAK,UAAU,QAAQ,CAAC,GAAW,EAAE,IAAsB,EAAE,QAA2B;IACtF,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;YACnC,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;YACvC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAA+B,EAAE,GAAW;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,SAAS,eAAe,CAAC,MAA+B,EAAE,GAAW;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpG,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an MCP endpoint says to a caller without a credential, read from one
|
|
3
|
+
* `initialize` and its status line and headers alone.
|
|
4
|
+
*
|
|
5
|
+
* Two processes ask this question and both need the same answer: the
|
|
6
|
+
* control plane at save time (does this URL-only server need Sign in, so
|
|
7
|
+
* the button appears before anyone runs it) and the runner at the first
|
|
8
|
+
* failed tool call (was that failure an OAuth requirement, so the user is
|
|
9
|
+
* told to sign in instead of to check a token). The catalogue audit asks
|
|
10
|
+
* it too, and then keeps asking (`tools/list`, the SSE arm, the metadata
|
|
11
|
+
* walk) because it measures; those extra moves are the audit's, built on
|
|
12
|
+
* `request.ts` and `metadata.ts`, not this function's.
|
|
13
|
+
*
|
|
14
|
+
* The outcome vocabulary is closed and says only what the wire said. A 2xx
|
|
15
|
+
* is `accepted`: the handshake was not refused for want of a credential,
|
|
16
|
+
* and whether the body carried a result or a JSON-RPC error is not this
|
|
17
|
+
* reader's question (both mean "not OAuth" to its callers). The body is
|
|
18
|
+
* cancelled unread, so a server that streams its answer over an event
|
|
19
|
+
* stream that never closes cannot hold the probe open. Only a 401 enters
|
|
20
|
+
* the OAuth rule (`challenge.ts`).
|
|
21
|
+
*
|
|
22
|
+
* Never throws: a network failure, an abort or a deadline is `unreachable`,
|
|
23
|
+
* because a caller that saves a resource or classifies a failure must not
|
|
24
|
+
* fail on a probe. No redirect is followed by the probe itself; the fetch
|
|
25
|
+
* it is handed decides that (the control plane's guarded fetch follows and
|
|
26
|
+
* re-judges each hop, the runner's `fetch` follows). The caller sets the
|
|
27
|
+
* deadline: 3 s on a user's save, 10 s on the runner's failure path.
|
|
28
|
+
*
|
|
29
|
+
* Proven by __tests__/probe.test.ts.
|
|
30
|
+
*/
|
|
31
|
+
import type { OutboundFetch } from "../egress/fetch.js";
|
|
32
|
+
export type EndpointAuthOutcome =
|
|
33
|
+
/** A 401 the OAuth rule accepts: the endpoint wants an OAuth token and says where its login server is described. */
|
|
34
|
+
{
|
|
35
|
+
readonly kind: "oauth";
|
|
36
|
+
readonly challenge: string;
|
|
37
|
+
readonly resourceMetadataUrl: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** A 401 the rule refuses: a static-token API's bad-key answer, or a proxy asking for its own credential. */
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: "challenge-not-oauth";
|
|
42
|
+
readonly challenge: string;
|
|
43
|
+
}
|
|
44
|
+
/** A 2xx: the handshake was not refused for want of a credential. */
|
|
45
|
+
| {
|
|
46
|
+
readonly kind: "accepted";
|
|
47
|
+
readonly status: number;
|
|
48
|
+
}
|
|
49
|
+
/** Any other HTTP status. */
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: "http-other";
|
|
52
|
+
readonly status: number;
|
|
53
|
+
}
|
|
54
|
+
/** No HTTP answer: a network failure, a refusal by the fetch, or the deadline. */
|
|
55
|
+
| {
|
|
56
|
+
readonly kind: "unreachable";
|
|
57
|
+
readonly error: string;
|
|
58
|
+
};
|
|
59
|
+
export interface EndpointProbeDeps {
|
|
60
|
+
readonly fetchImpl: OutboundFetch;
|
|
61
|
+
/** The whole probe's deadline, connection and headers included. */
|
|
62
|
+
readonly timeoutMs: number;
|
|
63
|
+
/** Who is asking, for the handshake's `clientInfo`. */
|
|
64
|
+
readonly clientName: string;
|
|
65
|
+
}
|
|
66
|
+
/** Ask an endpoint once, with the caller's headers, and say what it answered. */
|
|
67
|
+
export declare function probeEndpointAuth(url: string, headers: Readonly<Record<string, string>> | undefined, deps: EndpointProbeDeps): Promise<EndpointAuthOutcome>;
|
|
68
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/mcp-oauth/probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,MAAM,MAAM,mBAAmB;AAC7B,oHAAoH;AAClH;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE;AAC1G,6GAA6G;GAC3G;IAAE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACtE,qEAAqE;GACnE;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACxD,6BAA6B;GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC1D,kFAAkF;GAChF;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,iFAAiF;AACjF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EACrD,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,mBAAmB,CAAC,CA2B9B"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isOAuthChallenge, parseResourceMetadataUrl } from "./challenge.js";
|
|
2
|
+
import { initializeRequest } from "./request.js";
|
|
3
|
+
/** Ask an endpoint once, with the caller's headers, and say what it answered. */
|
|
4
|
+
export async function probeEndpointAuth(url, headers, deps) {
|
|
5
|
+
const request = initializeRequest(deps.clientName, headers);
|
|
6
|
+
let response;
|
|
7
|
+
try {
|
|
8
|
+
response = await deps.fetchImpl(url, {
|
|
9
|
+
method: request.method,
|
|
10
|
+
headers: { ...request.headers },
|
|
11
|
+
body: request.body,
|
|
12
|
+
signal: AbortSignal.timeout(deps.timeoutMs),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return { kind: "unreachable", error: describeError(error) };
|
|
17
|
+
}
|
|
18
|
+
// Only the status line and the headers classify; the body is released so
|
|
19
|
+
// an endless event stream cannot hold the probe open.
|
|
20
|
+
await response.body?.cancel().catch(() => undefined);
|
|
21
|
+
if (response.status === 401) {
|
|
22
|
+
const challenge = response.headers.get("www-authenticate") ?? "";
|
|
23
|
+
if (!isOAuthChallenge(challenge))
|
|
24
|
+
return { kind: "challenge-not-oauth", challenge };
|
|
25
|
+
return { kind: "oauth", challenge, resourceMetadataUrl: parseResourceMetadataUrl(challenge) };
|
|
26
|
+
}
|
|
27
|
+
if (response.status >= 200 && response.status < 300) {
|
|
28
|
+
return { kind: "accepted", status: response.status };
|
|
29
|
+
}
|
|
30
|
+
return { kind: "http-other", status: response.status };
|
|
31
|
+
}
|
|
32
|
+
function describeError(error) {
|
|
33
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/mcp-oauth/probe.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAsBjD,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,OAAqD,EACrD,IAAuB;IAEvB,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC5D,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;YACnC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE;YAC/B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;IAC9D,CAAC;IAED,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAErD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACjE,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,CAAC;QACpF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,wBAAwB,CAAC,SAAS,CAAC,EAAE,CAAC;IAChG,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACvD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACzD,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpF,CAAC"}
|