@mcp-z/client 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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":["DiscoveryFetchError","discoveryFetch","isLoopbackUrl","readDiscoveryJson","message","name","Error","DEFAULT_TIMEOUT_MS","MAX_REDIRECTS","MAX_BODY_BYTES","stripBrackets","hostname","startsWith","endsWith","slice","isLoopbackHost","host","toLowerCase","isIP","ipaddr","process","range","rawUrl","URL","isBlockedAddress","address","assertSafeUrl","context","allowLoopback","url","protocol","trustedLoopback","assertResolvesToSafeAddress","addresses","records","dns","lookup","all","verbatim","map","record","length","some","readLimited","response","maxBytes","contentLength","reader","text","chunks","total","done","value","headers","get","Number","body","getReader","Buffer","byteLength","read","push","releaseLock","concat","chunk","from","toString","fetchOnce","init","timeoutMs","controller","timeout","AbortController","setTimeout","abort","fetch","redirect","signal","clearTimeout","options","redirectsLeft","location","nextUrl","status","JSON","parse"],"mappings":"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"}
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';\nimport http from 'node:http';\nimport https from 'node:https';\nimport { isIP, type LookupFunction } 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\ntype LookupRecord = { address: string; family: number };\n/** DNS implementation, injectable for deterministic tests. Same contract as `dns.lookup` with `{ all: true, verbatim: true }`. */\ntype Lookup = (hostname: string, options: { all: true; verbatim: true }, callback: (error: NodeJS.ErrnoException | null, addresses: LookupRecord[]) => void) => void;\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\nfunction isLoopbackAddress(address: string): boolean {\n try {\n return ipaddr.process(address).range() === 'loopback';\n } catch {\n return false;\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\nfunction lookupAll(hostname: string, lookup: Lookup, timeoutMs: number): Promise<LookupRecord[]> {\n return new Promise((resolve, reject) => {\n let settled = false;\n const deadline = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error('DNS resolution timed out'));\n }\n }, timeoutMs);\n try {\n lookup(hostname, { all: true, verbatim: true }, (error, addresses) => {\n if (settled) return;\n settled = true;\n clearTimeout(deadline);\n if (error || addresses.length === 0) reject(new Error('DNS resolution failed'));\n else resolve(addresses);\n });\n } catch {\n if (!settled) {\n settled = true;\n clearTimeout(deadline);\n reject(new Error('DNS resolution failed'));\n }\n }\n });\n}\n\n/**\n * Resolves the hostname once, rejects it unless every returned address is\n * acceptable, and returns the validated set. The set is pinned into the\n * request itself (see `requestOnce`), which is what closes the DNS-rebinding\n * TOCTOU: the transport can only dial addresses that were already validated,\n * and it never resolves the hostname a second time.\n */\nasync function resolveSafeAddresses(url: URL, context: string, allowLoopback: boolean, lookup: Lookup, timeoutMs: number): Promise<LookupRecord[]> {\n const host = stripBrackets(url.hostname);\n if (isIP(host)) return [{ address: host, family: host.includes(':') ? 6 : 4 }]; // literal IP already fully checked in assertSafeUrl\n\n // Literal `localhost` under a trusted-loopback grant: still resolved, and the\n // answers must be loopback - the grant covers loopback, not \"wherever\n // localhost happens to point\".\n const loopbackGrant = allowLoopback && isLoopbackHost(host);\n\n let addresses: LookupRecord[];\n try {\n addresses = await lookupAll(host, lookup, timeoutMs);\n } catch {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host could not be resolved`);\n }\n\n if (loopbackGrant) {\n if (!addresses.every((record) => isLoopbackAddress(record.address))) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host does not resolve to a loopback address`);\n }\n } else if (addresses.some((record) => isBlockedAddress(record.address))) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host resolves to an address that is not publicly routable`);\n }\n\n return addresses;\n}\n\n/**\n * A `net` lookup that can only ever answer with `addresses` - the set already\n * validated for this URL. The request is built against the original URL (so\n * SNI and certificate verification still see the hostname), but the\n * connection can only land on a validated address.\n */\nfunction pinnedLookup(addresses: LookupRecord[]): LookupFunction {\n return (_hostname, options, callback) => {\n if (typeof options === 'object' && options.all === true) {\n callback(null, addresses);\n return;\n }\n const first = addresses[0];\n if (!first) {\n callback(Object.assign(new Error('no validated address'), { code: 'ENOTFOUND' }) as NodeJS.ErrnoException, '', 0);\n return;\n }\n callback(null, first.address, first.family);\n };\n}\n\nfunction toRequestHeaders(init: RequestInit): Record<string, string> {\n const headers = new Headers(init.headers);\n const out: Record<string, string> = {};\n for (const [name, value] of headers) out[name] = value;\n return out;\n}\n\nasync function toRequestBody(body: RequestInit['body']): Promise<Buffer | undefined> {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return Buffer.from(body);\n if (body instanceof URLSearchParams) return Buffer.from(body.toString());\n if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());\n if (body instanceof ArrayBuffer) return Buffer.from(body);\n if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n const reader = body.getReader();\n const chunks: Uint8Array[] = [];\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));\n }\n throw new Error('unsupported request body');\n}\n\nfunction toResponse(status: number, statusText: string, headers: http.IncomingHttpHeaders, body: Buffer): Response {\n const responseHeaders = new Headers();\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) responseHeaders.append(name, entry);\n } else {\n responseHeaders.append(name, value);\n }\n }\n // The fetch spec gives these statuses a null body, and the Response\n // constructor rejects a body for them.\n const nullBody = status === 204 || status === 205 || status === 304;\n return new Response(nullBody ? null : body, { status, statusText, headers: responseHeaders });\n}\n\nasync function requestOnce(url: URL, init: RequestInit, context: string, addresses: LookupRecord[], timeoutMs: number): Promise<Response> {\n let body: Buffer | undefined;\n try {\n body = await toRequestBody(init.body);\n } catch {\n throw new DiscoveryFetchError(`Failed to fetch ${context}`);\n }\n\n const transport = url.protocol === 'https:' ? https : http;\n const options: http.RequestOptions = { method: (init.method ?? 'GET').toUpperCase(), headers: toRequestHeaders(init), agent: false, lookup: pinnedLookup(addresses) };\n\n return new Promise<Response>((resolve, reject) => {\n let settled = false;\n let req: http.ClientRequest | undefined;\n let res: http.IncomingMessage | undefined;\n const fail = (message: string): void => {\n if (!settled) {\n settled = true;\n clearTimeout(deadline);\n reject(new DiscoveryFetchError(message));\n }\n };\n const deadline = setTimeout(() => {\n res?.destroy();\n req?.destroy();\n fail(`Failed to fetch ${context}`);\n }, timeoutMs);\n\n try {\n req = transport.request(url, options, (response) => {\n res = response;\n const status = response.statusCode;\n const statusText = response.statusMessage ?? '';\n if (status === undefined || status < 200 || status > 599) {\n response.destroy();\n fail(`Failed to fetch ${context}`);\n return;\n }\n const chunks: Buffer[] = [];\n let total = 0;\n response.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > MAX_BODY_BYTES) {\n response.destroy();\n fail(`Refusing to read ${context}: response too large`);\n return;\n }\n chunks.push(chunk);\n });\n response.once('error', () => fail(`Failed to fetch ${context}`));\n response.once('end', () => {\n if (settled) return;\n settled = true;\n clearTimeout(deadline);\n resolve(toResponse(status, statusText, response.headers, Buffer.concat(chunks)));\n });\n });\n } catch {\n fail(`Failed to fetch ${context}`);\n return;\n }\n\n req.once('error', () => fail(`Failed to fetch ${context}`));\n req.end(body);\n });\n}\n\nasync function fetchOnce(url: URL, init: RequestInit, context: string, allowLoopback: boolean, timeoutMs: number, lookup: Lookup): Promise<Response> {\n const addresses = await resolveSafeAddresses(url, context, allowLoopback, lookup, timeoutMs);\n return requestOnce(url, init, context, addresses, timeoutMs);\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-hop timeout in ms, applied to both the DNS resolution and the request.\n * Defaults to `DEFAULT_TIMEOUT_MS`; overridable so tests can bound slow-failure cases.\n */\n timeoutMs?: number;\n /**\n * DNS implementation used for the pre-request resolution, injectable for\n * deterministic tests. Defaults to `dns.lookup`.\n */\n lookup?: Lookup;\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 * The hostname is resolved once, every returned address is validated, and the\n * validated set is pinned into the request through a custom `lookup`, so the\n * request cannot resolve the hostname a second time and cannot be steered to\n * a different address by a DNS-rebinding answer.\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, lookup = dns.lookup as unknown as Lookup } = options;\n\n let url = assertSafeUrl(rawUrl, context, allowLoopback);\n let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs, lookup);\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, lookup);\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\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"],"names":["DiscoveryFetchError","discoveryFetch","isLoopbackUrl","readDiscoveryJson","message","name","Error","DEFAULT_TIMEOUT_MS","MAX_REDIRECTS","MAX_BODY_BYTES","stripBrackets","hostname","startsWith","endsWith","slice","isLoopbackHost","host","toLowerCase","isIP","ipaddr","process","range","rawUrl","URL","isBlockedAddress","address","isLoopbackAddress","assertSafeUrl","context","allowLoopback","url","protocol","trustedLoopback","lookupAll","lookup","timeoutMs","Promise","resolve","reject","settled","deadline","setTimeout","all","verbatim","error","addresses","clearTimeout","length","resolveSafeAddresses","loopbackGrant","family","includes","every","record","some","pinnedLookup","_hostname","options","callback","first","Object","assign","code","toRequestHeaders","init","headers","Headers","out","value","toRequestBody","body","reader","chunks","done","undefined","Buffer","from","URLSearchParams","toString","Blob","arrayBuffer","ArrayBuffer","isView","buffer","byteOffset","byteLength","ReadableStream","getReader","read","push","releaseLock","concat","map","chunk","toResponse","status","statusText","responseHeaders","entries","Array","isArray","entry","append","nullBody","Response","requestOnce","transport","https","http","method","toUpperCase","agent","req","res","fail","destroy","request","response","statusCode","statusMessage","total","on","once","end","fetchOnce","redirectsLeft","location","nextUrl","dns","get","text","readLimited","JSON","parse","maxBytes","contentLength","Number"],"mappings":"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"}
@@ -1,6 +1,15 @@
1
1
  export declare class DiscoveryFetchError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
+ type LookupRecord = {
5
+ address: string;
6
+ family: number;
7
+ };
8
+ /** DNS implementation, injectable for deterministic tests. Same contract as `dns.lookup` with `{ all: true, verbatim: true }`. */
9
+ type Lookup = (hostname: string, options: {
10
+ all: true;
11
+ verbatim: true;
12
+ }, callback: (error: NodeJS.ErrnoException | null, addresses: LookupRecord[]) => void) => void;
4
13
  /**
5
14
  * True if `rawUrl`'s host is loopback, used to decide `allowLoopback` grants
6
15
  * from the URL the caller configured, not remote-supplied data. Fails closed on an unparseable URL.
@@ -13,15 +22,25 @@ export interface DiscoveryFetchOptions {
13
22
  */
14
23
  allowLoopback?: boolean;
15
24
  /**
16
- * Per-request connect/read timeout in ms. Defaults to `DEFAULT_TIMEOUT_MS`;
17
- * overridable so tests can bound slow-failure cases.
25
+ * Per-hop timeout in ms, applied to both the DNS resolution and the request.
26
+ * Defaults to `DEFAULT_TIMEOUT_MS`; overridable so tests can bound slow-failure cases.
18
27
  */
19
28
  timeoutMs?: number;
29
+ /**
30
+ * DNS implementation used for the pre-request resolution, injectable for
31
+ * deterministic tests. Defaults to `dns.lookup`.
32
+ */
33
+ lookup?: Lookup;
20
34
  }
21
35
  /**
22
36
  * Fetches an OAuth-discovery URL with SSRF mitigations applied to the
23
37
  * initial URL and every redirect hop; redirects are validated, not auto-followed.
24
38
  *
39
+ * The hostname is resolved once, every returned address is validated, and the
40
+ * validated set is pinned into the request through a custom `lookup`, so the
41
+ * request cannot resolve the hostname a second time and cannot be steered to
42
+ * a different address by a DNS-rebinding answer.
43
+ *
25
44
  * @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).
26
45
  * @param init - Standard fetch options; `redirect` is always forced to `'manual'`.
27
46
  * @param context - Short label used only in error messages, never echoing the URL.
@@ -33,3 +52,4 @@ export declare function discoveryFetch(rawUrl: string, init?: RequestInit, conte
33
52
  * while reading (not after buffering the whole thing).
34
53
  */
35
54
  export declare function readDiscoveryJson<T>(response: Response, context: string): Promise<T>;
55
+ export {};
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Hardened fetch for OAuth discovery URLs, which the remote MCP server
3
3
  * controls (SSRF mitigation). See ARCHITECTURE.md's "SSRF mitigation" section for the threat model.
4
- */ import dns from 'node:dns/promises';
4
+ */ import dns from 'node:dns';
5
+ import http from 'node:http';
6
+ import https from 'node:https';
5
7
  import { isIP } from 'node:net';
6
8
  import ipaddr from 'ipaddr.js';
7
9
  export class DiscoveryFetchError extends Error {
@@ -46,6 +48,13 @@ function stripBrackets(hostname) {
46
48
  return true; // unparseable - fail closed
47
49
  }
48
50
  }
51
+ function isLoopbackAddress(address) {
52
+ try {
53
+ return ipaddr.process(address).range() === 'loopback';
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
49
58
  /**
50
59
  * Validates scheme and, for literal-IP hosts, address range. `allowLoopback`
51
60
  * reflects trust in the calling server, never in `rawUrl` itself.
@@ -70,88 +79,227 @@ function stripBrackets(hostname) {
70
79
  }
71
80
  return url;
72
81
  }
82
+ function lookupAll(hostname, lookup, timeoutMs) {
83
+ return new Promise((resolve, reject)=>{
84
+ let settled = false;
85
+ const deadline = setTimeout(()=>{
86
+ if (!settled) {
87
+ settled = true;
88
+ reject(new Error('DNS resolution timed out'));
89
+ }
90
+ }, timeoutMs);
91
+ try {
92
+ lookup(hostname, {
93
+ all: true,
94
+ verbatim: true
95
+ }, (error, addresses)=>{
96
+ if (settled) return;
97
+ settled = true;
98
+ clearTimeout(deadline);
99
+ if (error || addresses.length === 0) reject(new Error('DNS resolution failed'));
100
+ else resolve(addresses);
101
+ });
102
+ } catch {
103
+ if (!settled) {
104
+ settled = true;
105
+ clearTimeout(deadline);
106
+ reject(new Error('DNS resolution failed'));
107
+ }
108
+ }
109
+ });
110
+ }
73
111
  /**
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) {
112
+ * Resolves the hostname once, rejects it unless every returned address is
113
+ * acceptable, and returns the validated set. The set is pinned into the
114
+ * request itself (see `requestOnce`), which is what closes the DNS-rebinding
115
+ * TOCTOU: the transport can only dial addresses that were already validated,
116
+ * and it never resolves the hostname a second time.
117
+ */ async function resolveSafeAddresses(url, context, allowLoopback, lookup, timeoutMs) {
77
118
  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
119
+ if (isIP(host)) return [
120
+ {
121
+ address: host,
122
+ family: host.includes(':') ? 6 : 4
123
+ }
124
+ ]; // literal IP already fully checked in assertSafeUrl
125
+ // Literal `localhost` under a trusted-loopback grant: still resolved, and the
126
+ // answers must be loopback - the grant covers loopback, not "wherever
127
+ // localhost happens to point".
128
+ const loopbackGrant = allowLoopback && isLoopbackHost(host);
80
129
  let addresses;
81
130
  try {
82
- const records = await dns.lookup(host, {
83
- all: true,
84
- verbatim: true
85
- });
86
- addresses = records.map((record)=>record.address);
131
+ addresses = await lookupAll(host, lookup, timeoutMs);
87
132
  } catch {
88
133
  throw new DiscoveryFetchError(`Refusing to fetch ${context}: host could not be resolved`);
89
134
  }
90
- if (addresses.length === 0 || addresses.some((address)=>isBlockedAddress(address))) {
135
+ if (loopbackGrant) {
136
+ if (!addresses.every((record)=>isLoopbackAddress(record.address))) {
137
+ throw new DiscoveryFetchError(`Refusing to fetch ${context}: host does not resolve to a loopback address`);
138
+ }
139
+ } else if (addresses.some((record)=>isBlockedAddress(record.address))) {
91
140
  throw new DiscoveryFetchError(`Refusing to fetch ${context}: host resolves to an address that is not publicly routable`);
92
141
  }
142
+ return addresses;
93
143
  }
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`);
144
+ /**
145
+ * A `net` lookup that can only ever answer with `addresses` - the set already
146
+ * validated for this URL. The request is built against the original URL (so
147
+ * SNI and certificate verification still see the hostname), but the
148
+ * connection can only land on a validated address.
149
+ */ function pinnedLookup(addresses) {
150
+ return (_hostname, options, callback)=>{
151
+ if (typeof options === 'object' && options.all === true) {
152
+ callback(null, addresses);
153
+ return;
105
154
  }
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`);
155
+ const first = addresses[0];
156
+ if (!first) {
157
+ callback(Object.assign(new Error('no validated address'), {
158
+ code: 'ENOTFOUND'
159
+ }), '', 0);
160
+ return;
161
+ }
162
+ callback(null, first.address, first.family);
163
+ };
164
+ }
165
+ function toRequestHeaders(init) {
166
+ const headers = new Headers(init.headers);
167
+ const out = {};
168
+ for (const [name, value] of headers)out[name] = value;
169
+ return out;
170
+ }
171
+ async function toRequestBody(body) {
172
+ if (body === undefined || body === null) return undefined;
173
+ if (typeof body === 'string') return Buffer.from(body);
174
+ if (body instanceof URLSearchParams) return Buffer.from(body.toString());
175
+ if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());
176
+ if (body instanceof ArrayBuffer) return Buffer.from(body);
177
+ if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
178
+ if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
179
+ const reader = body.getReader();
180
+ const chunks = [];
181
+ try {
182
+ for(;;){
183
+ const { done, value } = await reader.read();
184
+ if (done) break;
185
+ if (value) chunks.push(value);
118
186
  }
119
- chunks.push(value);
187
+ } finally{
188
+ reader.releaseLock();
120
189
  }
121
- } finally{
122
- reader.releaseLock();
190
+ return Buffer.concat(chunks.map((chunk)=>Buffer.from(chunk)));
123
191
  }
124
- return Buffer.concat(chunks.map((chunk)=>Buffer.from(chunk))).toString('utf8');
192
+ throw new Error('unsupported request body');
193
+ }
194
+ function toResponse(status, statusText, headers, body) {
195
+ const responseHeaders = new Headers();
196
+ for (const [name, value] of Object.entries(headers)){
197
+ if (value === undefined) continue;
198
+ if (Array.isArray(value)) {
199
+ for (const entry of value)responseHeaders.append(name, entry);
200
+ } else {
201
+ responseHeaders.append(name, value);
202
+ }
203
+ }
204
+ // The fetch spec gives these statuses a null body, and the Response
205
+ // constructor rejects a body for them.
206
+ const nullBody = status === 204 || status === 205 || status === 304;
207
+ return new Response(nullBody ? null : body, {
208
+ status,
209
+ statusText,
210
+ headers: responseHeaders
211
+ });
125
212
  }
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);
213
+ async function requestOnce(url, init, context, addresses, timeoutMs) {
214
+ var _init_method;
215
+ let body;
130
216
  try {
131
- return await fetch(url, {
132
- ...init,
133
- redirect: 'manual',
134
- signal: controller.signal
135
- });
217
+ body = await toRequestBody(init.body);
136
218
  } catch {
137
- // Never surface the underlying error (ECONNREFUSED host/port, DNS detail, etc.)
138
219
  throw new DiscoveryFetchError(`Failed to fetch ${context}`);
139
- } finally{
140
- clearTimeout(timeout);
141
220
  }
221
+ const transport = url.protocol === 'https:' ? https : http;
222
+ const options = {
223
+ method: ((_init_method = init.method) !== null && _init_method !== void 0 ? _init_method : 'GET').toUpperCase(),
224
+ headers: toRequestHeaders(init),
225
+ agent: false,
226
+ lookup: pinnedLookup(addresses)
227
+ };
228
+ return new Promise((resolve, reject)=>{
229
+ let settled = false;
230
+ let req;
231
+ let res;
232
+ const fail = (message)=>{
233
+ if (!settled) {
234
+ settled = true;
235
+ clearTimeout(deadline);
236
+ reject(new DiscoveryFetchError(message));
237
+ }
238
+ };
239
+ const deadline = setTimeout(()=>{
240
+ res === null || res === void 0 ? void 0 : res.destroy();
241
+ req === null || req === void 0 ? void 0 : req.destroy();
242
+ fail(`Failed to fetch ${context}`);
243
+ }, timeoutMs);
244
+ try {
245
+ req = transport.request(url, options, (response)=>{
246
+ var _response_statusMessage;
247
+ res = response;
248
+ const status = response.statusCode;
249
+ const statusText = (_response_statusMessage = response.statusMessage) !== null && _response_statusMessage !== void 0 ? _response_statusMessage : '';
250
+ if (status === undefined || status < 200 || status > 599) {
251
+ response.destroy();
252
+ fail(`Failed to fetch ${context}`);
253
+ return;
254
+ }
255
+ const chunks = [];
256
+ let total = 0;
257
+ response.on('data', (chunk)=>{
258
+ total += chunk.length;
259
+ if (total > MAX_BODY_BYTES) {
260
+ response.destroy();
261
+ fail(`Refusing to read ${context}: response too large`);
262
+ return;
263
+ }
264
+ chunks.push(chunk);
265
+ });
266
+ response.once('error', ()=>fail(`Failed to fetch ${context}`));
267
+ response.once('end', ()=>{
268
+ if (settled) return;
269
+ settled = true;
270
+ clearTimeout(deadline);
271
+ resolve(toResponse(status, statusText, response.headers, Buffer.concat(chunks)));
272
+ });
273
+ });
274
+ } catch {
275
+ fail(`Failed to fetch ${context}`);
276
+ return;
277
+ }
278
+ req.once('error', ()=>fail(`Failed to fetch ${context}`));
279
+ req.end(body);
280
+ });
281
+ }
282
+ async function fetchOnce(url, init, context, allowLoopback, timeoutMs, lookup) {
283
+ const addresses = await resolveSafeAddresses(url, context, allowLoopback, lookup, timeoutMs);
284
+ return requestOnce(url, init, context, addresses, timeoutMs);
142
285
  }
143
286
  /**
144
287
  * Fetches an OAuth-discovery URL with SSRF mitigations applied to the
145
288
  * initial URL and every redirect hop; redirects are validated, not auto-followed.
146
289
  *
290
+ * The hostname is resolved once, every returned address is validated, and the
291
+ * validated set is pinned into the request through a custom `lookup`, so the
292
+ * request cannot resolve the hostname a second time and cannot be steered to
293
+ * a different address by a DNS-rebinding answer.
294
+ *
147
295
  * @param rawUrl - URL to fetch; may be remote-server-supplied (the threat this guards against).
148
296
  * @param init - Standard fetch options; `redirect` is always forced to `'manual'`.
149
297
  * @param context - Short label used only in error messages, never echoing the URL.
150
298
  * @param options - See `DiscoveryFetchOptions`.
151
299
  */ export async function discoveryFetch(rawUrl, init = {}, context = 'discovery URL', options = {}) {
152
- const { allowLoopback = false, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
300
+ const { allowLoopback = false, timeoutMs = DEFAULT_TIMEOUT_MS, lookup = dns.lookup } = options;
153
301
  let url = assertSafeUrl(rawUrl, context, allowLoopback);
154
- let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);
302
+ let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs, lookup);
155
303
  let redirectsLeft = MAX_REDIRECTS;
156
304
  while(response.status >= 300 && response.status < 400){
157
305
  const location = response.headers.get('location');
@@ -167,7 +315,7 @@ async function fetchOnce(url, init, context, allowLoopback, timeoutMs) {
167
315
  throw new DiscoveryFetchError(`Refusing to fetch ${context}: invalid redirect target`);
168
316
  }
169
317
  url = assertSafeUrl(nextUrl.toString(), context, allowLoopback);
170
- response = await fetchOnce(url, init, context, allowLoopback, timeoutMs);
318
+ response = await fetchOnce(url, init, context, allowLoopback, timeoutMs, lookup);
171
319
  }
172
320
  return response;
173
321
  }
@@ -182,3 +330,35 @@ async function fetchOnce(url, init, context, allowLoopback, timeoutMs) {
182
330
  throw new DiscoveryFetchError(`Refusing to parse ${context}: invalid JSON`);
183
331
  }
184
332
  }
333
+ async function readLimited(response, context, maxBytes) {
334
+ var _response_body;
335
+ const contentLength = response.headers.get('content-length');
336
+ if (contentLength && Number(contentLength) > maxBytes) {
337
+ throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
338
+ }
339
+ const reader = (_response_body = response.body) === null || _response_body === void 0 ? void 0 : _response_body.getReader();
340
+ if (!reader) {
341
+ const text = await response.text();
342
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) {
343
+ throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
344
+ }
345
+ return text;
346
+ }
347
+ const chunks = [];
348
+ let total = 0;
349
+ try {
350
+ for(;;){
351
+ const { done, value } = await reader.read();
352
+ if (done) break;
353
+ if (!value) continue;
354
+ total += value.byteLength;
355
+ if (total > maxBytes) {
356
+ throw new DiscoveryFetchError(`Refusing to read ${context}: response too large`);
357
+ }
358
+ chunks.push(value);
359
+ }
360
+ } finally{
361
+ reader.releaseLock();
362
+ }
363
+ return Buffer.concat(chunks.map((chunk)=>Buffer.from(chunk))).toString('utf8');
364
+ }
@@ -1 +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":"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"}
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';\nimport http from 'node:http';\nimport https from 'node:https';\nimport { isIP, type LookupFunction } 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\ntype LookupRecord = { address: string; family: number };\n/** DNS implementation, injectable for deterministic tests. Same contract as `dns.lookup` with `{ all: true, verbatim: true }`. */\ntype Lookup = (hostname: string, options: { all: true; verbatim: true }, callback: (error: NodeJS.ErrnoException | null, addresses: LookupRecord[]) => void) => void;\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\nfunction isLoopbackAddress(address: string): boolean {\n try {\n return ipaddr.process(address).range() === 'loopback';\n } catch {\n return false;\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\nfunction lookupAll(hostname: string, lookup: Lookup, timeoutMs: number): Promise<LookupRecord[]> {\n return new Promise((resolve, reject) => {\n let settled = false;\n const deadline = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error('DNS resolution timed out'));\n }\n }, timeoutMs);\n try {\n lookup(hostname, { all: true, verbatim: true }, (error, addresses) => {\n if (settled) return;\n settled = true;\n clearTimeout(deadline);\n if (error || addresses.length === 0) reject(new Error('DNS resolution failed'));\n else resolve(addresses);\n });\n } catch {\n if (!settled) {\n settled = true;\n clearTimeout(deadline);\n reject(new Error('DNS resolution failed'));\n }\n }\n });\n}\n\n/**\n * Resolves the hostname once, rejects it unless every returned address is\n * acceptable, and returns the validated set. The set is pinned into the\n * request itself (see `requestOnce`), which is what closes the DNS-rebinding\n * TOCTOU: the transport can only dial addresses that were already validated,\n * and it never resolves the hostname a second time.\n */\nasync function resolveSafeAddresses(url: URL, context: string, allowLoopback: boolean, lookup: Lookup, timeoutMs: number): Promise<LookupRecord[]> {\n const host = stripBrackets(url.hostname);\n if (isIP(host)) return [{ address: host, family: host.includes(':') ? 6 : 4 }]; // literal IP already fully checked in assertSafeUrl\n\n // Literal `localhost` under a trusted-loopback grant: still resolved, and the\n // answers must be loopback - the grant covers loopback, not \"wherever\n // localhost happens to point\".\n const loopbackGrant = allowLoopback && isLoopbackHost(host);\n\n let addresses: LookupRecord[];\n try {\n addresses = await lookupAll(host, lookup, timeoutMs);\n } catch {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host could not be resolved`);\n }\n\n if (loopbackGrant) {\n if (!addresses.every((record) => isLoopbackAddress(record.address))) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host does not resolve to a loopback address`);\n }\n } else if (addresses.some((record) => isBlockedAddress(record.address))) {\n throw new DiscoveryFetchError(`Refusing to fetch ${context}: host resolves to an address that is not publicly routable`);\n }\n\n return addresses;\n}\n\n/**\n * A `net` lookup that can only ever answer with `addresses` - the set already\n * validated for this URL. The request is built against the original URL (so\n * SNI and certificate verification still see the hostname), but the\n * connection can only land on a validated address.\n */\nfunction pinnedLookup(addresses: LookupRecord[]): LookupFunction {\n return (_hostname, options, callback) => {\n if (typeof options === 'object' && options.all === true) {\n callback(null, addresses);\n return;\n }\n const first = addresses[0];\n if (!first) {\n callback(Object.assign(new Error('no validated address'), { code: 'ENOTFOUND' }) as NodeJS.ErrnoException, '', 0);\n return;\n }\n callback(null, first.address, first.family);\n };\n}\n\nfunction toRequestHeaders(init: RequestInit): Record<string, string> {\n const headers = new Headers(init.headers);\n const out: Record<string, string> = {};\n for (const [name, value] of headers) out[name] = value;\n return out;\n}\n\nasync function toRequestBody(body: RequestInit['body']): Promise<Buffer | undefined> {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return Buffer.from(body);\n if (body instanceof URLSearchParams) return Buffer.from(body.toString());\n if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());\n if (body instanceof ArrayBuffer) return Buffer.from(body);\n if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n const reader = body.getReader();\n const chunks: Uint8Array[] = [];\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));\n }\n throw new Error('unsupported request body');\n}\n\nfunction toResponse(status: number, statusText: string, headers: http.IncomingHttpHeaders, body: Buffer): Response {\n const responseHeaders = new Headers();\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) responseHeaders.append(name, entry);\n } else {\n responseHeaders.append(name, value);\n }\n }\n // The fetch spec gives these statuses a null body, and the Response\n // constructor rejects a body for them.\n const nullBody = status === 204 || status === 205 || status === 304;\n return new Response(nullBody ? null : body, { status, statusText, headers: responseHeaders });\n}\n\nasync function requestOnce(url: URL, init: RequestInit, context: string, addresses: LookupRecord[], timeoutMs: number): Promise<Response> {\n let body: Buffer | undefined;\n try {\n body = await toRequestBody(init.body);\n } catch {\n throw new DiscoveryFetchError(`Failed to fetch ${context}`);\n }\n\n const transport = url.protocol === 'https:' ? https : http;\n const options: http.RequestOptions = { method: (init.method ?? 'GET').toUpperCase(), headers: toRequestHeaders(init), agent: false, lookup: pinnedLookup(addresses) };\n\n return new Promise<Response>((resolve, reject) => {\n let settled = false;\n let req: http.ClientRequest | undefined;\n let res: http.IncomingMessage | undefined;\n const fail = (message: string): void => {\n if (!settled) {\n settled = true;\n clearTimeout(deadline);\n reject(new DiscoveryFetchError(message));\n }\n };\n const deadline = setTimeout(() => {\n res?.destroy();\n req?.destroy();\n fail(`Failed to fetch ${context}`);\n }, timeoutMs);\n\n try {\n req = transport.request(url, options, (response) => {\n res = response;\n const status = response.statusCode;\n const statusText = response.statusMessage ?? '';\n if (status === undefined || status < 200 || status > 599) {\n response.destroy();\n fail(`Failed to fetch ${context}`);\n return;\n }\n const chunks: Buffer[] = [];\n let total = 0;\n response.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > MAX_BODY_BYTES) {\n response.destroy();\n fail(`Refusing to read ${context}: response too large`);\n return;\n }\n chunks.push(chunk);\n });\n response.once('error', () => fail(`Failed to fetch ${context}`));\n response.once('end', () => {\n if (settled) return;\n settled = true;\n clearTimeout(deadline);\n resolve(toResponse(status, statusText, response.headers, Buffer.concat(chunks)));\n });\n });\n } catch {\n fail(`Failed to fetch ${context}`);\n return;\n }\n\n req.once('error', () => fail(`Failed to fetch ${context}`));\n req.end(body);\n });\n}\n\nasync function fetchOnce(url: URL, init: RequestInit, context: string, allowLoopback: boolean, timeoutMs: number, lookup: Lookup): Promise<Response> {\n const addresses = await resolveSafeAddresses(url, context, allowLoopback, lookup, timeoutMs);\n return requestOnce(url, init, context, addresses, timeoutMs);\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-hop timeout in ms, applied to both the DNS resolution and the request.\n * Defaults to `DEFAULT_TIMEOUT_MS`; overridable so tests can bound slow-failure cases.\n */\n timeoutMs?: number;\n /**\n * DNS implementation used for the pre-request resolution, injectable for\n * deterministic tests. Defaults to `dns.lookup`.\n */\n lookup?: Lookup;\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 * The hostname is resolved once, every returned address is validated, and the\n * validated set is pinned into the request through a custom `lookup`, so the\n * request cannot resolve the hostname a second time and cannot be steered to\n * a different address by a DNS-rebinding answer.\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, lookup = dns.lookup as unknown as Lookup } = options;\n\n let url = assertSafeUrl(rawUrl, context, allowLoopback);\n let response = await fetchOnce(url, init, context, allowLoopback, timeoutMs, lookup);\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, lookup);\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\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"],"names":["dns","http","https","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","isLoopbackAddress","assertSafeUrl","context","allowLoopback","url","protocol","trustedLoopback","lookupAll","lookup","timeoutMs","Promise","resolve","reject","settled","deadline","setTimeout","all","verbatim","error","addresses","clearTimeout","length","resolveSafeAddresses","family","includes","loopbackGrant","every","record","some","pinnedLookup","_hostname","options","callback","first","Object","assign","code","toRequestHeaders","init","headers","Headers","out","value","toRequestBody","body","undefined","Buffer","from","URLSearchParams","toString","Blob","arrayBuffer","ArrayBuffer","isView","buffer","byteOffset","byteLength","ReadableStream","reader","getReader","chunks","done","read","push","releaseLock","concat","map","chunk","toResponse","status","statusText","responseHeaders","entries","Array","isArray","entry","append","nullBody","Response","requestOnce","transport","method","toUpperCase","agent","req","res","fail","destroy","request","response","statusCode","statusMessage","total","on","once","end","fetchOnce","discoveryFetch","redirectsLeft","location","get","nextUrl","readDiscoveryJson","text","readLimited","JSON","parse","maxBytes","contentLength","Number"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-z/client",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Programmatic MCP client library for Node.js - connect, discover, and call tools on Model Context Protocol servers.",
5
5
  "keywords": [
6
6
  "mcp",