import { isIP } from "node:net";
import { promisify } from "node:util";
import { lookup } from "node:dns";
//#region src/media-shared/http/ssrf-guard.ts
/**
* SSRF guard for voice / media-understanding HTTP calls.
*
* DECISION (per docs/voice-rearchitecture.md §6):
*  - xopc does not currently ship the full openclaw SSRF infra (pinned dispatcher,
*    legacy private-network opt-in, etc.). For v2.0 we adopt a deliberately lean
*    guard that is sufficient for "developer-controlled provider base URLs":
*      1. Reject non-http(s) schemes.
*      2. Reject IP literals or hostnames that resolve to private / loopback /
*         link-local / metadata addresses unless `allowPrivateNetwork` is true.
*      3. Resolve the hostname BEFORE handing the request to fetch (DNS pinning)
*         and reuse the resolved IP for the actual connection — closes the
*         classic DNS-rebinding window without requiring a custom dispatcher.
*  - Why not port openclaw's pinned dispatcher (undici): xopc is committed to
*    plain `globalThis.fetch` (Node 22 native) per AGENTS.md "Node >= 22"; we
*    accept the slightly weaker guarantee in exchange for zero new dependencies.
*  - Tests under src/media-shared/http/__tests__/ssrf-guard.test.ts cover all
*    private CIDR ranges + IPv6 link-local + metadata IPs (169.254.169.254).
*/
const dnsLookupAsync = promisify(lookup);
var SsrfBlockedError = class extends Error {
	url;
	reason;
	constructor(url, reason) {
		super(`SSRF guard blocked URL "${url}": ${reason}`);
		this.name = "SsrfBlockedError";
		this.url = url;
		this.reason = reason;
	}
};
const PRIVATE_IPV4_CIDR_PATTERNS = [
	(parts) => parts[0] === 10,
	(parts) => parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31,
	(parts) => parts[0] === 192 && parts[1] === 168,
	(parts) => parts[0] === 127,
	(parts) => parts[0] === 169 && parts[1] === 254,
	(parts) => parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127,
	(parts) => parts[0] === 0
];
function isPrivateIpv4(ip) {
	if (isIP(ip) !== 4) return false;
	const parts = ip.split(".").map((octet) => Number.parseInt(octet, 10));
	if (parts.length !== 4 || parts.some((octet) => Number.isNaN(octet))) return false;
	for (const matcher of PRIVATE_IPV4_CIDR_PATTERNS) if (matcher(parts)) return true;
	return false;
}
function isPrivateIpv6(ip) {
	if (isIP(ip) !== 6) return false;
	const lower = ip.toLowerCase();
	if (lower === "::1" || lower === "0:0:0:0:0:0:0:1") return true;
	if (/^f[cd][0-9a-f]{2}:/.test(lower)) return true;
	if (/^fe[89ab][0-9a-f]:/.test(lower)) return true;
	const v4Mapped = lower.match(/^::ffff:([0-9.]+)$/);
	if (v4Mapped && isIP(v4Mapped[1]) === 4) return isPrivateIpv4(v4Mapped[1]);
	return false;
}
function isPrivateIpAddress(ip) {
	return isPrivateIpv4(ip) || isPrivateIpv6(ip);
}
/**
* Validate `url`, resolve its hostname via DNS, and return the resolved IP so
* the caller can pin the request to that exact IP (closes DNS-rebinding window).
*
* Throws SsrfBlockedError when:
*  - scheme is not http(s)
*  - hostname is an explicit IP literal that targets a private range (and the
*    caller did not opt in)
*  - hostname resolves to a private IP (and the caller did not opt in)
*  - hostnameAllowlist is set and the hostname is not on it
*/
async function assertSafeUrl(url, options = {}) {
	const parsed = url instanceof URL ? url : new URL(url);
	const urlString = parsed.toString();
	if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new SsrfBlockedError(urlString, `unsupported scheme "${parsed.protocol}"`);
	const hostname = parsed.hostname;
	if (!hostname) throw new SsrfBlockedError(urlString, "missing hostname");
	if (options.hostnameAllowlist && options.hostnameAllowlist.length > 0) {
		const lower = hostname.toLowerCase();
		if (!options.hostnameAllowlist.some((entry) => lower === entry.toLowerCase())) throw new SsrfBlockedError(urlString, `hostname "${hostname}" not in allowlist`);
	}
	const literalFamily = isIP(hostname);
	if (literalFamily !== 0) {
		if (!options.allowPrivateNetwork && isPrivateIpAddress(hostname)) throw new SsrfBlockedError(urlString, `IP literal "${hostname}" targets a private network`);
		return {
			url: urlString,
			resolvedIp: hostname,
			family: literalFamily
		};
	}
	let address;
	let family;
	try {
		const resolved = await dnsLookupAsync(hostname);
		address = resolved.address;
		family = resolved.family;
	} catch (error) {
		throw new SsrfBlockedError(urlString, `DNS lookup failed: ${error instanceof Error ? error.message : String(error)}`);
	}
	if (!options.allowPrivateNetwork && isPrivateIpAddress(address)) throw new SsrfBlockedError(urlString, `hostname "${hostname}" resolved to private IP "${address}"`);
	return {
		url: urlString,
		resolvedIp: address,
		family
	};
}
//#endregion
export { SsrfBlockedError, assertSafeUrl, isPrivateIpAddress, isPrivateIpv4, isPrivateIpv6 };

//# sourceMappingURL=ssrf-guard.js.map