@mcp-z/client 1.1.0 → 1.2.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/README.md +5 -2
- package/dist/cjs/auth/capability-discovery.js +13 -7
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/discovery-fetch.d.cts +35 -0
- package/dist/cjs/auth/discovery-fetch.d.ts +35 -0
- package/dist/cjs/auth/discovery-fetch.js +658 -0
- package/dist/cjs/auth/discovery-fetch.js.map +1 -0
- package/dist/cjs/auth/index.d.cts +1 -0
- package/dist/cjs/auth/index.d.ts +1 -0
- package/dist/cjs/auth/index.js +7 -0
- package/dist/cjs/auth/index.js.map +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.cts +12 -12
- package/dist/cjs/auth/interactive-oauth-flow.d.ts +12 -12
- package/dist/cjs/auth/interactive-oauth-flow.js +22 -16
- package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/cjs/auth/rfc9728-discovery.d.cts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.js +59 -48
- package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
- package/dist/cjs/auth/types.d.cts +16 -0
- package/dist/cjs/auth/types.d.ts +16 -0
- package/dist/cjs/auth/types.js.map +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.cts +3 -4
- package/dist/cjs/dcr/dcr-authenticator.d.ts +3 -4
- package/dist/cjs/dcr/dcr-authenticator.js +22 -12
- package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
- package/dist/cjs/dcr/dynamic-client-registrar.d.cts +6 -17
- package/dist/cjs/dcr/dynamic-client-registrar.d.ts +6 -17
- package/dist/cjs/dcr/dynamic-client-registrar.js +10 -16
- package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/auth/capability-discovery.js +12 -6
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/discovery-fetch.d.ts +35 -0
- package/dist/esm/auth/discovery-fetch.js +184 -0
- package/dist/esm/auth/discovery-fetch.js.map +1 -0
- package/dist/esm/auth/index.d.ts +1 -0
- package/dist/esm/auth/index.js +1 -0
- package/dist/esm/auth/index.js.map +1 -1
- package/dist/esm/auth/interactive-oauth-flow.d.ts +12 -12
- package/dist/esm/auth/interactive-oauth-flow.js +25 -16
- package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/esm/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/esm/auth/rfc9728-discovery.js +49 -56
- package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
- package/dist/esm/auth/types.d.ts +16 -0
- package/dist/esm/auth/types.js.map +1 -1
- package/dist/esm/dcr/dcr-authenticator.d.ts +3 -4
- package/dist/esm/dcr/dcr-authenticator.js +20 -11
- package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
- package/dist/esm/dcr/dynamic-client-registrar.d.ts +6 -17
- package/dist/esm/dcr/dynamic-client-registrar.js +13 -18
- package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +2 -7
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardened fetch for OAuth discovery URLs, which the remote MCP server
|
|
3
|
+
* controls (SSRF mitigation). See ARCHITECTURE.md's "SSRF mitigation" section for the threat model.
|
|
4
|
+
*/ import dns from 'node:dns/promises';
|
|
5
|
+
import { isIP } from 'node:net';
|
|
6
|
+
import ipaddr from 'ipaddr.js';
|
|
7
|
+
export class DiscoveryFetchError extends Error {
|
|
8
|
+
constructor(message){
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'DiscoveryFetchError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
14
|
+
const MAX_REDIRECTS = 5;
|
|
15
|
+
const MAX_BODY_BYTES = 1000000; // 1 MB - discovery documents are small JSON
|
|
16
|
+
function stripBrackets(hostname) {
|
|
17
|
+
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
18
|
+
}
|
|
19
|
+
/** True for the literal hostname `localhost` or an IP literal in the loopback range. */ function isLoopbackHost(hostname) {
|
|
20
|
+
const host = stripBrackets(hostname).toLowerCase();
|
|
21
|
+
if (host === 'localhost') return true;
|
|
22
|
+
if (isIP(host)) {
|
|
23
|
+
try {
|
|
24
|
+
return ipaddr.process(host).range() === 'loopback';
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* True if `rawUrl`'s host is loopback, used to decide `allowLoopback` grants
|
|
33
|
+
* from the URL the caller configured, not remote-supplied data. Fails closed on an unparseable URL.
|
|
34
|
+
*/ export function isLoopbackUrl(rawUrl) {
|
|
35
|
+
try {
|
|
36
|
+
return isLoopbackHost(new URL(rawUrl).hostname);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** True when the address is anything other than ordinary public unicast space. */ function isBlockedAddress(address) {
|
|
42
|
+
try {
|
|
43
|
+
// ipaddr.process() normalizes IPv4-mapped IPv6 (::ffff:a.b.c.d) to IPv4 first.
|
|
44
|
+
return ipaddr.process(address).range() !== 'unicast';
|
|
45
|
+
} catch {
|
|
46
|
+
return true; // unparseable - fail closed
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validates scheme and, for literal-IP hosts, address range. `allowLoopback`
|
|
51
|
+
* reflects trust in the calling server, never in `rawUrl` itself.
|
|
52
|
+
*/ function assertSafeUrl(rawUrl, context, allowLoopback) {
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(rawUrl);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: invalid URL`);
|
|
58
|
+
}
|
|
59
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
60
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: unsupported scheme`);
|
|
61
|
+
}
|
|
62
|
+
const host = stripBrackets(url.hostname);
|
|
63
|
+
const trustedLoopback = allowLoopback && isLoopbackHost(host);
|
|
64
|
+
if (trustedLoopback) return url; // this call's own grant covers it, regardless of scheme
|
|
65
|
+
if (url.protocol === 'http:') {
|
|
66
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: http:// is only allowed for a trusted loopback origin`);
|
|
67
|
+
}
|
|
68
|
+
if (isIP(host) && isBlockedAddress(host)) {
|
|
69
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: address is not publicly routable`);
|
|
70
|
+
}
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the hostname and rejects it if any address is private/reserved.
|
|
75
|
+
* Does not close the DNS-rebinding TOCTOU race against `fetch`'s own resolution a moment later.
|
|
76
|
+
*/ async function assertResolvesToSafeAddress(url, context, allowLoopback) {
|
|
77
|
+
const host = stripBrackets(url.hostname);
|
|
78
|
+
if (isIP(host)) return; // literal IP already fully checked in assertSafeUrl
|
|
79
|
+
if (allowLoopback && isLoopbackHost(host)) return; // literal "localhost" under a trusted-loopback grant
|
|
80
|
+
let addresses;
|
|
81
|
+
try {
|
|
82
|
+
const records = await dns.lookup(host, {
|
|
83
|
+
all: true,
|
|
84
|
+
verbatim: true
|
|
85
|
+
});
|
|
86
|
+
addresses = records.map((record)=>record.address);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: host could not be resolved`);
|
|
89
|
+
}
|
|
90
|
+
if (addresses.length === 0 || addresses.some((address)=>isBlockedAddress(address))) {
|
|
91
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: host resolves to an address that is not publicly routable`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function readLimited(response, context, maxBytes) {
|
|
95
|
+
var _response_body;
|
|
96
|
+
const contentLength = response.headers.get('content-length');
|
|
97
|
+
if (contentLength && Number(contentLength) > maxBytes) {
|
|
98
|
+
throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
|
|
99
|
+
}
|
|
100
|
+
const reader = (_response_body = response.body) === null || _response_body === void 0 ? void 0 : _response_body.getReader();
|
|
101
|
+
if (!reader) {
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
|
|
104
|
+
throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
|
|
105
|
+
}
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
const chunks = [];
|
|
109
|
+
let total = 0;
|
|
110
|
+
try {
|
|
111
|
+
for(;;){
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
if (!value) continue;
|
|
115
|
+
total += value.byteLength;
|
|
116
|
+
if (total > maxBytes) {
|
|
117
|
+
throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
|
|
118
|
+
}
|
|
119
|
+
chunks.push(value);
|
|
120
|
+
}
|
|
121
|
+
} finally{
|
|
122
|
+
reader.releaseLock();
|
|
123
|
+
}
|
|
124
|
+
return Buffer.concat(chunks.map((chunk)=>Buffer.from(chunk))).toString('utf8');
|
|
125
|
+
}
|
|
126
|
+
async function fetchOnce(url, init, context, allowLoopback, timeoutMs) {
|
|
127
|
+
await assertResolvesToSafeAddress(url, context, allowLoopback);
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timeout = setTimeout(()=>controller.abort(), timeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
return await fetch(url, {
|
|
132
|
+
...init,
|
|
133
|
+
redirect: 'manual',
|
|
134
|
+
signal: controller.signal
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
// Never surface the underlying error (ECONNREFUSED host/port, DNS detail, etc.)
|
|
138
|
+
throw new DiscoveryFetchError(`Failed to fetch ${context}`);
|
|
139
|
+
} finally{
|
|
140
|
+
clearTimeout(timeout);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fetches an OAuth-discovery URL with SSRF mitigations applied to the
|
|
145
|
+
* initial URL and every redirect hop; redirects are validated, not auto-followed.
|
|
146
|
+
*
|
|
147
|
+
* @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).
|
|
148
|
+
* @param init - Standard fetch options; `redirect` is always forced to `'manual'`.
|
|
149
|
+
* @param context - Short label used only in error messages, never echoing the URL.
|
|
150
|
+
* @param options - See `DiscoveryFetchOptions`.
|
|
151
|
+
*/ export async function discoveryFetch(rawUrl, init = {}, context = 'discovery URL', options = {}) {
|
|
152
|
+
const { allowLoopback = false, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
153
|
+
let url = assertSafeUrl(rawUrl, context, allowLoopback);
|
|
154
|
+
let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);
|
|
155
|
+
let redirectsLeft = MAX_REDIRECTS;
|
|
156
|
+
while(response.status >= 300 && response.status < 400){
|
|
157
|
+
const location = response.headers.get('location');
|
|
158
|
+
if (!location) break;
|
|
159
|
+
if (redirectsLeft <= 0) {
|
|
160
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: too many redirects`);
|
|
161
|
+
}
|
|
162
|
+
redirectsLeft -= 1;
|
|
163
|
+
let nextUrl;
|
|
164
|
+
try {
|
|
165
|
+
nextUrl = new URL(location, url);
|
|
166
|
+
} catch {
|
|
167
|
+
throw new DiscoveryFetchError(`Refusing to fetch ${context}: invalid redirect target`);
|
|
168
|
+
}
|
|
169
|
+
url = assertSafeUrl(nextUrl.toString(), context, allowLoopback);
|
|
170
|
+
response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);
|
|
171
|
+
}
|
|
172
|
+
return response;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse a `discoveryFetch` response body as JSON, enforcing `MAX_BODY_BYTES`
|
|
176
|
+
* while reading (not after buffering the whole thing).
|
|
177
|
+
*/ export async function readDiscoveryJson(response, context) {
|
|
178
|
+
const text = await readLimited(response, context, MAX_BODY_BYTES);
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(text);
|
|
181
|
+
} catch {
|
|
182
|
+
throw new DiscoveryFetchError(`Refusing to parse ${context}: invalid JSON`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/discovery-fetch.ts"],"sourcesContent":["/**\n * Hardened fetch for OAuth discovery URLs, which the remote MCP server\n * controls (SSRF mitigation). See ARCHITECTURE.md's \"SSRF mitigation\" section for the threat model.\n */\nimport dns from 'node:dns/promises';\nimport { isIP } from 'node:net';\nimport ipaddr from 'ipaddr.js';\n\nexport class DiscoveryFetchError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'DiscoveryFetchError';\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_REDIRECTS = 5;\nconst MAX_BODY_BYTES = 1_000_000; // 1 MB - discovery documents are small JSON\n\nfunction stripBrackets(hostname: string): string {\n return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n}\n\n/** True for the literal hostname `localhost` or an IP literal in the loopback range. */\nfunction isLoopbackHost(hostname: string): boolean {\n const host = stripBrackets(hostname).toLowerCase();\n if (host === 'localhost') return true;\n if (isIP(host)) {\n try {\n return ipaddr.process(host).range() === 'loopback';\n } catch {\n return false;\n }\n }\n return false;\n}\n\n/**\n * True if `rawUrl`'s host is loopback, used to decide `allowLoopback` grants\n * from the URL the caller configured, not remote-supplied data. Fails closed on an unparseable URL.\n */\nexport function isLoopbackUrl(rawUrl: string): boolean {\n try {\n return isLoopbackHost(new URL(rawUrl).hostname);\n } catch {\n return false;\n }\n}\n\n/** True when the address is anything other than ordinary public unicast space. */\nfunction isBlockedAddress(address: string): boolean {\n try {\n // ipaddr.process() normalizes IPv4-mapped IPv6 (::ffff:a.b.c.d) to IPv4 first.\n return ipaddr.process(address).range() !== 'unicast';\n } catch {\n return true; // unparseable - fail closed\n }\n}\n\n/**\n * Validates scheme and, for literal-IP hosts, address range. `allowLoopback`\n * reflects trust in the calling server, never in `rawUrl` itself.\n */\nfunction assertSafeUrl(rawUrl: string, context: string, allowLoopback: boolean): URL {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: invalid URL`);\n }\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: unsupported scheme`);\n }\n\n const host = stripBrackets(url.hostname);\n const trustedLoopback = allowLoopback && isLoopbackHost(host);\n if (trustedLoopback) return url; // this call's own grant covers it, regardless of scheme\n\n if (url.protocol === 'http:') {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: http:// is only allowed for a trusted loopback origin`);\n }\n\n if (isIP(host) && isBlockedAddress(host)) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: address is not publicly routable`);\n }\n\n return url;\n}\n\n/**\n * Resolves the hostname and rejects it if any address is private/reserved.\n * Does not close the DNS-rebinding TOCTOU race against `fetch`'s own resolution a moment later.\n */\nasync function assertResolvesToSafeAddress(url: URL, context: string, allowLoopback: boolean): Promise<void> {\n const host = stripBrackets(url.hostname);\n if (isIP(host)) return; // literal IP already fully checked in assertSafeUrl\n if (allowLoopback && isLoopbackHost(host)) return; // literal \"localhost\" under a trusted-loopback grant\n\n let addresses: string[];\n try {\n const records = await dns.lookup(host, { all: true, verbatim: true });\n addresses = records.map((record) => record.address);\n } catch {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host could not be resolved`);\n }\n\n if (addresses.length === 0 || addresses.some((address) => isBlockedAddress(address))) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host resolves to an address that is not publicly routable`);\n }\n}\n\nasync function readLimited(response: Response, context: string, maxBytes: number): Promise<string> {\n const contentLength = response.headers.get('content-length');\n if (contentLength && Number(contentLength) > maxBytes) {\n throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n const text = await response.text();\n if (Buffer.byteLength(text, 'utf8') > maxBytes) {\n throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);\n }\n return text;\n }\n\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n total += value.byteLength;\n if (total > maxBytes) {\n throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n\n return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8');\n}\n\nasync function fetchOnce(url: URL, init: RequestInit, context: string, allowLoopback: boolean, timeoutMs: number): Promise<Response> {\n await assertResolvesToSafeAddress(url, context, allowLoopback);\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, redirect: 'manual', signal: controller.signal });\n } catch {\n // Never surface the underlying error (ECONNREFUSED host/port, DNS detail, etc.)\n throw new DiscoveryFetchError(`Failed to fetch ${context}`);\n } finally {\n clearTimeout(timeout);\n }\n}\n\nexport interface DiscoveryFetchOptions {\n /**\n * Loopback trust grant for this fetch, computed from the server the caller\n * is actually talking to - never from the URL being fetched. Defaults to `false`.\n */\n allowLoopback?: boolean;\n /**\n * Per-request connect/read timeout in ms. Defaults to `DEFAULT_TIMEOUT_MS`;\n * overridable so tests can bound slow-failure cases.\n */\n timeoutMs?: number;\n}\n\n/**\n * Fetches an OAuth-discovery URL with SSRF mitigations applied to the\n * initial URL and every redirect hop; redirects are validated, not auto-followed.\n *\n * @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).\n * @param init - Standard fetch options; `redirect` is always forced to `'manual'`.\n * @param context - Short label used only in error messages, never echoing the URL.\n * @param options - See `DiscoveryFetchOptions`.\n */\nexport async function discoveryFetch(rawUrl: string, init: RequestInit = {}, context = 'discovery URL', options: DiscoveryFetchOptions = {}): Promise<Response> {\n const { allowLoopback = false, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n let url = assertSafeUrl(rawUrl, context, allowLoopback);\n let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);\n\n let redirectsLeft = MAX_REDIRECTS;\n while (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location');\n if (!location) break;\n if (redirectsLeft <= 0) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: too many redirects`);\n }\n redirectsLeft -= 1;\n\n let nextUrl: URL;\n try {\n nextUrl = new URL(location, url);\n } catch {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: invalid redirect target`);\n }\n url = assertSafeUrl(nextUrl.toString(), context, allowLoopback);\n response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);\n }\n\n return response;\n}\n\n/**\n * Parse a `discoveryFetch` response body as JSON, enforcing `MAX_BODY_BYTES`\n * while reading (not after buffering the whole thing).\n */\nexport async function readDiscoveryJson<T>(response: Response, context: string): Promise<T> {\n const text = await readLimited(response, context, MAX_BODY_BYTES);\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new DiscoveryFetchError(`Refusing to parse ${context}: invalid JSON`);\n }\n}\n"],"names":["dns","isIP","ipaddr","DiscoveryFetchError","Error","message","name","DEFAULT_TIMEOUT_MS","MAX_REDIRECTS","MAX_BODY_BYTES","stripBrackets","hostname","startsWith","endsWith","slice","isLoopbackHost","host","toLowerCase","process","range","isLoopbackUrl","rawUrl","URL","isBlockedAddress","address","assertSafeUrl","context","allowLoopback","url","protocol","trustedLoopback","assertResolvesToSafeAddress","addresses","records","lookup","all","verbatim","map","record","length","some","readLimited","response","maxBytes","contentLength","headers","get","Number","reader","body","getReader","text","Buffer","byteLength","chunks","total","done","value","read","push","releaseLock","concat","chunk","from","toString","fetchOnce","init","timeoutMs","controller","AbortController","timeout","setTimeout","abort","fetch","redirect","signal","clearTimeout","discoveryFetch","options","redirectsLeft","status","location","nextUrl","readDiscoveryJson","JSON","parse"],"mappings":"AAAA;;;CAGC,GACD,OAAOA,SAAS,oBAAoB;AACpC,SAASC,IAAI,QAAQ,WAAW;AAChC,OAAOC,YAAY,YAAY;AAE/B,OAAO,MAAMC,4BAA4BC;IACvC,YAAYC,OAAe,CAAE;QAC3B,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,MAAMC,qBAAqB;AAC3B,MAAMC,gBAAgB;AACtB,MAAMC,iBAAiB,SAAW,4CAA4C;AAE9E,SAASC,cAAcC,QAAgB;IACrC,OAAOA,SAASC,UAAU,CAAC,QAAQD,SAASE,QAAQ,CAAC,OAAOF,SAASG,KAAK,CAAC,GAAG,CAAC,KAAKH;AACtF;AAEA,sFAAsF,GACtF,SAASI,eAAeJ,QAAgB;IACtC,MAAMK,OAAON,cAAcC,UAAUM,WAAW;IAChD,IAAID,SAAS,aAAa,OAAO;IACjC,IAAIf,KAAKe,OAAO;QACd,IAAI;YACF,OAAOd,OAAOgB,OAAO,CAACF,MAAMG,KAAK,OAAO;QAC1C,EAAE,OAAM;YACN,OAAO;QACT;IACF;IACA,OAAO;AACT;AAEA;;;CAGC,GACD,OAAO,SAASC,cAAcC,MAAc;IAC1C,IAAI;QACF,OAAON,eAAe,IAAIO,IAAID,QAAQV,QAAQ;IAChD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,gFAAgF,GAChF,SAASY,iBAAiBC,OAAe;IACvC,IAAI;QACF,+EAA+E;QAC/E,OAAOtB,OAAOgB,OAAO,CAACM,SAASL,KAAK,OAAO;IAC7C,EAAE,OAAM;QACN,OAAO,MAAM,4BAA4B;IAC3C;AACF;AAEA;;;CAGC,GACD,SAASM,cAAcJ,MAAc,EAAEK,OAAe,EAAEC,aAAsB;IAC5E,IAAIC;IACJ,IAAI;QACFA,MAAM,IAAIN,IAAID;IAChB,EAAE,OAAM;QACN,MAAM,IAAIlB,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,aAAa,CAAC;IAC3E;IAEA,IAAIE,IAAIC,QAAQ,KAAK,YAAYD,IAAIC,QAAQ,KAAK,SAAS;QACzD,MAAM,IAAI1B,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,oBAAoB,CAAC;IAClF;IAEA,MAAMV,OAAON,cAAckB,IAAIjB,QAAQ;IACvC,MAAMmB,kBAAkBH,iBAAiBZ,eAAeC;IACxD,IAAIc,iBAAiB,OAAOF,KAAK,wDAAwD;IAEzF,IAAIA,IAAIC,QAAQ,KAAK,SAAS;QAC5B,MAAM,IAAI1B,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,uDAAuD,CAAC;IACrH;IAEA,IAAIzB,KAAKe,SAASO,iBAAiBP,OAAO;QACxC,MAAM,IAAIb,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,kCAAkC,CAAC;IAChG;IAEA,OAAOE;AACT;AAEA;;;CAGC,GACD,eAAeG,4BAA4BH,GAAQ,EAAEF,OAAe,EAAEC,aAAsB;IAC1F,MAAMX,OAAON,cAAckB,IAAIjB,QAAQ;IACvC,IAAIV,KAAKe,OAAO,QAAQ,oDAAoD;IAC5E,IAAIW,iBAAiBZ,eAAeC,OAAO,QAAQ,qDAAqD;IAExG,IAAIgB;IACJ,IAAI;QACF,MAAMC,UAAU,MAAMjC,IAAIkC,MAAM,CAAClB,MAAM;YAAEmB,KAAK;YAAMC,UAAU;QAAK;QACnEJ,YAAYC,QAAQI,GAAG,CAAC,CAACC,SAAWA,OAAOd,OAAO;IACpD,EAAE,OAAM;QACN,MAAM,IAAIrB,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,4BAA4B,CAAC;IAC1F;IAEA,IAAIM,UAAUO,MAAM,KAAK,KAAKP,UAAUQ,IAAI,CAAC,CAAChB,UAAYD,iBAAiBC,WAAW;QACpF,MAAM,IAAIrB,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,2DAA2D,CAAC;IACzH;AACF;AAEA,eAAee,YAAYC,QAAkB,EAAEhB,OAAe,EAAEiB,QAAgB;QAM/DD;IALf,MAAME,gBAAgBF,SAASG,OAAO,CAACC,GAAG,CAAC;IAC3C,IAAIF,iBAAiBG,OAAOH,iBAAiBD,UAAU;QACrD,MAAM,IAAIxC,oBAAoB,CAAC,iBAAiB,EAAEuB,QAAQ,oBAAoB,CAAC;IACjF;IAEA,MAAMsB,UAASN,iBAAAA,SAASO,IAAI,cAAbP,qCAAAA,eAAeQ,SAAS;IACvC,IAAI,CAACF,QAAQ;QACX,MAAMG,OAAO,MAAMT,SAASS,IAAI;QAChC,IAAIC,OAAOC,UAAU,CAACF,MAAM,UAAUR,UAAU;YAC9C,MAAM,IAAIxC,oBAAoB,CAAC,iBAAiB,EAAEuB,QAAQ,oBAAoB,CAAC;QACjF;QACA,OAAOyB;IACT;IAEA,MAAMG,SAAuB,EAAE;IAC/B,IAAIC,QAAQ;IACZ,IAAI;QACF,OAAS;YACP,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAE,GAAG,MAAMT,OAAOU,IAAI;YACzC,IAAIF,MAAM;YACV,IAAI,CAACC,OAAO;YACZF,SAASE,MAAMJ,UAAU;YACzB,IAAIE,QAAQZ,UAAU;gBACpB,MAAM,IAAIxC,oBAAoB,CAAC,iBAAiB,EAAEuB,QAAQ,oBAAoB,CAAC;YACjF;YACA4B,OAAOK,IAAI,CAACF;QACd;IACF,SAAU;QACRT,OAAOY,WAAW;IACpB;IAEA,OAAOR,OAAOS,MAAM,CAACP,OAAOjB,GAAG,CAAC,CAACyB,QAAUV,OAAOW,IAAI,CAACD,SAASE,QAAQ,CAAC;AAC3E;AAEA,eAAeC,UAAUrC,GAAQ,EAAEsC,IAAiB,EAAExC,OAAe,EAAEC,aAAsB,EAAEwC,SAAiB;IAC9G,MAAMpC,4BAA4BH,KAAKF,SAASC;IAEhD,MAAMyC,aAAa,IAAIC;IACvB,MAAMC,UAAUC,WAAW,IAAMH,WAAWI,KAAK,IAAIL;IACrD,IAAI;QACF,OAAO,MAAMM,MAAM7C,KAAK;YAAE,GAAGsC,IAAI;YAAEQ,UAAU;YAAUC,QAAQP,WAAWO,MAAM;QAAC;IACnF,EAAE,OAAM;QACN,gFAAgF;QAChF,MAAM,IAAIxE,oBAAoB,CAAC,gBAAgB,EAAEuB,SAAS;IAC5D,SAAU;QACRkD,aAAaN;IACf;AACF;AAeA;;;;;;;;CAQC,GACD,OAAO,eAAeO,eAAexD,MAAc,EAAE6C,OAAoB,CAAC,CAAC,EAAExC,UAAU,eAAe,EAAEoD,UAAiC,CAAC,CAAC;IACzI,MAAM,EAAEnD,gBAAgB,KAAK,EAAEwC,YAAY5D,kBAAkB,EAAE,GAAGuE;IAElE,IAAIlD,MAAMH,cAAcJ,QAAQK,SAASC;IACzC,IAAIe,WAAW,MAAMuB,UAAUrC,KAAKsC,MAAMxC,SAASC,eAAewC;IAElE,IAAIY,gBAAgBvE;IACpB,MAAOkC,SAASsC,MAAM,IAAI,OAAOtC,SAASsC,MAAM,GAAG,IAAK;QACtD,MAAMC,WAAWvC,SAASG,OAAO,CAACC,GAAG,CAAC;QACtC,IAAI,CAACmC,UAAU;QACf,IAAIF,iBAAiB,GAAG;YACtB,MAAM,IAAI5E,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,oBAAoB,CAAC;QAClF;QACAqD,iBAAiB;QAEjB,IAAIG;QACJ,IAAI;YACFA,UAAU,IAAI5D,IAAI2D,UAAUrD;QAC9B,EAAE,OAAM;YACN,MAAM,IAAIzB,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,yBAAyB,CAAC;QACvF;QACAE,MAAMH,cAAcyD,QAAQlB,QAAQ,IAAItC,SAASC;QACjDe,WAAW,MAAMuB,UAAUrC,KAAKsC,MAAMxC,SAASC,eAAewC;IAChE;IAEA,OAAOzB;AACT;AAEA;;;CAGC,GACD,OAAO,eAAeyC,kBAAqBzC,QAAkB,EAAEhB,OAAe;IAC5E,MAAMyB,OAAO,MAAMV,YAAYC,UAAUhB,SAASjB;IAClD,IAAI;QACF,OAAO2E,KAAKC,KAAK,CAAClC;IACpB,EAAE,OAAM;QACN,MAAM,IAAIhD,oBAAoB,CAAC,kBAAkB,EAAEuB,QAAQ,cAAc,CAAC;IAC5E;AACF"}
|
package/dist/esm/auth/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Exports public API for OAuth authentication only (DCR moved to ../dcr/)
|
|
4
4
|
*/
|
|
5
5
|
export { probeAuthCapabilities } from './capability-discovery.js';
|
|
6
|
+
export { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.js';
|
|
6
7
|
export { InteractiveOAuthFlow } from './interactive-oauth-flow.js';
|
|
7
8
|
export type { OAuthCallbackListenerOptions } from './oauth-callback-listener.js';
|
|
8
9
|
export { OAuthCallbackListener } from './oauth-callback-listener.js';
|
package/dist/esm/auth/index.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* Authentication Module
|
|
3
3
|
* Exports public API for OAuth authentication only (DCR moved to ../dcr/)
|
|
4
4
|
*/ export { probeAuthCapabilities } from './capability-discovery.js';
|
|
5
|
+
export { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.js';
|
|
5
6
|
export { InteractiveOAuthFlow } from './interactive-oauth-flow.js';
|
|
6
7
|
export { OAuthCallbackListener } from './oauth-callback-listener.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/index.ts"],"sourcesContent":["/**\n * Authentication Module\n * Exports public API for OAuth authentication only (DCR moved to ../dcr/)\n */\n\nexport { probeAuthCapabilities } from './capability-discovery.ts';\nexport { InteractiveOAuthFlow } from './interactive-oauth-flow.ts';\nexport type { OAuthCallbackListenerOptions } from './oauth-callback-listener.ts';\nexport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nexport type { AuthCapabilities, CallbackResult, OAuthFlowOptions, TokenSet } from './types.ts';\n"],"names":["probeAuthCapabilities","InteractiveOAuthFlow","OAuthCallbackListener"],"mappings":"AAAA;;;CAGC,GAED,SAASA,qBAAqB,QAAQ,4BAA4B;AAClE,SAASC,oBAAoB,QAAQ,8BAA8B;AAEnE,SAASC,qBAAqB,QAAQ,+BAA+B"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/index.ts"],"sourcesContent":["/**\n * Authentication Module\n * Exports public API for OAuth authentication only (DCR moved to ../dcr/)\n */\n\nexport { probeAuthCapabilities } from './capability-discovery.ts';\nexport { DiscoveryFetchError, isLoopbackUrl } from './discovery-fetch.ts';\nexport { InteractiveOAuthFlow } from './interactive-oauth-flow.ts';\nexport type { OAuthCallbackListenerOptions } from './oauth-callback-listener.ts';\nexport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nexport type { AuthCapabilities, CallbackResult, OAuthFlowOptions, TokenSet } from './types.ts';\n"],"names":["probeAuthCapabilities","DiscoveryFetchError","isLoopbackUrl","InteractiveOAuthFlow","OAuthCallbackListener"],"mappings":"AAAA;;;CAGC,GAED,SAASA,qBAAqB,QAAQ,4BAA4B;AAClE,SAASC,mBAAmB,EAAEC,aAAa,QAAQ,uBAAuB;AAC1E,SAASC,oBAAoB,QAAQ,8BAA8B;AAEnE,SAASC,qBAAqB,QAAQ,+BAA+B"}
|
|
@@ -34,22 +34,22 @@ export declare class InteractiveOAuthFlow {
|
|
|
34
34
|
*/
|
|
35
35
|
performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet>;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param
|
|
37
|
+
* Exchanges an authorization code for access and refresh tokens.
|
|
38
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
39
|
+
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
39
40
|
*/
|
|
40
41
|
private exchangeCodeForTokens;
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
46
|
-
* @param
|
|
47
|
-
* @param
|
|
48
|
-
* @returns New token set with refreshed access token
|
|
49
|
-
*
|
|
50
|
-
* @throws Error if refresh fails
|
|
43
|
+
* Refreshes an access token using a refresh token.
|
|
44
|
+
* @param tokenEndpoint - OAuth token endpoint URL.
|
|
45
|
+
* @param refreshToken - Refresh token from a previous token set.
|
|
46
|
+
* @param clientId - OAuth client ID.
|
|
47
|
+
* @param clientSecret - OAuth client secret.
|
|
48
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
49
|
+
* @returns New token set with a refreshed access token.
|
|
50
|
+
* @throws Error if refresh fails.
|
|
51
51
|
*/
|
|
52
|
-
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string): Promise<TokenSet>;
|
|
52
|
+
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, allowLoopback?: boolean): Promise<TokenSet>;
|
|
53
53
|
/**
|
|
54
54
|
* Open browser to authorization URL
|
|
55
55
|
* Uses platform-specific command to open default browser
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Manages browser-based OAuth flows and token exchange with PKCE support
|
|
4
4
|
*/ import * as child_process from 'node:child_process';
|
|
5
5
|
import { logger as defaultLogger } from '../utils/logger.js';
|
|
6
|
+
import { discoveryFetch } from './discovery-fetch.js';
|
|
6
7
|
import { OAuthCallbackListener } from './oauth-callback-listener.js';
|
|
7
8
|
import { generatePkce } from './pkce.js';
|
|
8
9
|
/**
|
|
@@ -46,6 +47,7 @@ import { generatePkce } from './pkce.js';
|
|
|
46
47
|
pkce = await generatePkce();
|
|
47
48
|
}
|
|
48
49
|
try {
|
|
50
|
+
var _options_allowLoopback;
|
|
49
51
|
// Start callback server
|
|
50
52
|
await callbackListener.start();
|
|
51
53
|
// Build redirect URI
|
|
@@ -81,7 +83,7 @@ import { generatePkce } from './pkce.js';
|
|
|
81
83
|
const timeout = options.timeout || (options.headless ? 60000 : 300000);
|
|
82
84
|
const result = await callbackListener.waitForCallback(timeout);
|
|
83
85
|
// Exchange authorization code for tokens (with PKCE verifier if used)
|
|
84
|
-
const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier);
|
|
86
|
+
const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, (_options_allowLoopback = options.allowLoopback) !== null && _options_allowLoopback !== void 0 ? _options_allowLoopback : false, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier);
|
|
85
87
|
return tokens;
|
|
86
88
|
} catch (error) {
|
|
87
89
|
logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));
|
|
@@ -92,9 +94,10 @@ import { generatePkce } from './pkce.js';
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @param
|
|
97
|
-
|
|
97
|
+
* Exchanges an authorization code for access and refresh tokens.
|
|
98
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
99
|
+
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
100
|
+
*/ async exchangeCodeForTokens(tokenEndpoint, code, clientId, clientSecret, redirectUri, allowLoopback, codeVerifier) {
|
|
98
101
|
const params = new URLSearchParams({
|
|
99
102
|
grant_type: 'authorization_code',
|
|
100
103
|
code,
|
|
@@ -106,7 +109,9 @@ import { generatePkce } from './pkce.js';
|
|
|
106
109
|
if (codeVerifier) {
|
|
107
110
|
params.set('code_verifier', codeVerifier);
|
|
108
111
|
}
|
|
109
|
-
|
|
112
|
+
// tokenEndpoint is remote-controlled discovery data; discoveryFetch blocks
|
|
113
|
+
// a private/internal target before the client secret is sent to it.
|
|
114
|
+
const response = await discoveryFetch(tokenEndpoint, {
|
|
110
115
|
method: 'POST',
|
|
111
116
|
headers: {
|
|
112
117
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -114,6 +119,8 @@ import { generatePkce } from './pkce.js';
|
|
|
114
119
|
Connection: 'close'
|
|
115
120
|
},
|
|
116
121
|
body: params
|
|
122
|
+
}, 'token endpoint', {
|
|
123
|
+
allowLoopback
|
|
117
124
|
});
|
|
118
125
|
if (!response.ok) {
|
|
119
126
|
const errorText = await response.text();
|
|
@@ -136,17 +143,17 @@ import { generatePkce } from './pkce.js';
|
|
|
136
143
|
return tokenSet;
|
|
137
144
|
}
|
|
138
145
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param
|
|
142
|
-
* @param
|
|
143
|
-
* @param
|
|
144
|
-
* @param
|
|
145
|
-
* @returns New token set with refreshed access token
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const response = await
|
|
146
|
+
* Refreshes an access token using a refresh token.
|
|
147
|
+
* @param tokenEndpoint - OAuth token endpoint URL.
|
|
148
|
+
* @param refreshToken - Refresh token from a previous token set.
|
|
149
|
+
* @param clientId - OAuth client ID.
|
|
150
|
+
* @param clientSecret - OAuth client secret.
|
|
151
|
+
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
152
|
+
* @returns New token set with a refreshed access token.
|
|
153
|
+
* @throws Error if refresh fails.
|
|
154
|
+
*/ async refreshTokens(tokenEndpoint, refreshToken, clientId, clientSecret, allowLoopback = false) {
|
|
155
|
+
// See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.
|
|
156
|
+
const response = await discoveryFetch(tokenEndpoint, {
|
|
150
157
|
method: 'POST',
|
|
151
158
|
headers: {
|
|
152
159
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -159,6 +166,8 @@ import { generatePkce } from './pkce.js';
|
|
|
159
166
|
client_id: clientId,
|
|
160
167
|
client_secret: clientSecret
|
|
161
168
|
})
|
|
169
|
+
}, 'token endpoint', {
|
|
170
|
+
allowLoopback
|
|
162
171
|
});
|
|
163
172
|
if (!response.ok) {
|
|
164
173
|
const errorText = await response.text();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger } from '../utils/logger.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Add resource parameter if specified (RFC 8707)\n if (options.resource) {\n authUrl.searchParams.set('resource', options.resource);\n }\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Exchange authorization code for access and refresh tokens\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636)\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param refreshToken - Refresh token from previous token set\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @returns New token set with refreshed access token\n *\n * @throws Error if refresh fails\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string): Promise<TokenSet> {\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["child_process","logger","defaultLogger","OAuthCallbackListener","generatePkce","InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","callbackListener","port","pkce","debug","start","redirectUri","authUrl","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","timeout","result","waitForCallback","tokens","exchangeCodeForTokens","code","codeVerifier","error","Error","message","String","stop","params","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","response","fetch","method","headers","Accept","Connection","body","ok","errorText","text","status","data","json","access_token","tokenSet","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","process","command","args","child","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger } from '../utils/logger.ts';\nimport { discoveryFetch } from './discovery-fetch.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Add resource parameter if specified (RFC 8707)\n if (options.resource) {\n authUrl.searchParams.set('resource', options.resource);\n }\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.allowLoopback ?? false, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Exchanges an authorization code for access and refresh tokens.\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636).\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, allowLoopback: boolean, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n // tokenEndpoint is remote-controlled discovery data; discoveryFetch blocks\n // a private/internal target before the client secret is sent to it.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param tokenEndpoint - OAuth token endpoint URL.\n * @param refreshToken - Refresh token from a previous token set.\n * @param clientId - OAuth client ID.\n * @param clientSecret - OAuth client secret.\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.\n * @returns New token set with a refreshed access token.\n * @throws Error if refresh fails.\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, allowLoopback = false): Promise<TokenSet> {\n // See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["child_process","logger","defaultLogger","discoveryFetch","OAuthCallbackListener","generatePkce","InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","callbackListener","port","pkce","debug","start","redirectUri","authUrl","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","timeout","result","waitForCallback","tokens","exchangeCodeForTokens","code","allowLoopback","codeVerifier","error","Error","message","String","stop","params","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","response","method","headers","Accept","Connection","body","ok","errorText","text","status","data","json","access_token","tokenSet","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","process","command","args","child","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
@@ -4,37 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
11
|
-
* Probes .well-known/oauth-protected-resource endpoint
|
|
7
|
+
* Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`
|
|
8
|
+
* header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
|
|
16
|
-
*
|
|
17
|
-
* @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
|
|
18
|
-
* @returns ProtectedResourceMetadata if discovered, null otherwise
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
|
|
22
|
-
* const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
|
|
23
|
-
* // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
|
|
10
|
+
* @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).
|
|
11
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
24
12
|
*/
|
|
25
13
|
export declare function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null>;
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
|
|
31
|
-
* @returns AuthorizationServerMetadata if discovered, null otherwise
|
|
15
|
+
* Discovers RFC 8414 Authorization Server Metadata at
|
|
16
|
+
* `.well-known/oauth-authorization-server`, path-local variant first.
|
|
32
17
|
*
|
|
33
|
-
* @
|
|
34
|
-
*
|
|
35
|
-
*
|
|
18
|
+
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).
|
|
19
|
+
* @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.
|
|
20
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
36
21
|
*/
|
|
37
|
-
export declare function discoverAuthorizationServerMetadata(authServerUrl: string
|
|
22
|
+
export declare function discoverAuthorizationServerMetadata(authServerUrl: string, options?: {
|
|
23
|
+
allowLoopback?: boolean;
|
|
24
|
+
}): Promise<AuthorizationServerMetadata | null>;
|
|
38
25
|
/**
|
|
39
26
|
* Discover OAuth Authorization Server Issuer from resource response (RFC 9207)
|
|
40
27
|
*
|