import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { isIPv4, isIPv6 } from "node:net";
//#region src/agent/tools/url-safety.ts
/**
* URL safety checks for web tools — blocks requests to private/internal
* network addresses and cloud metadata endpoints (SSRF protection).
*
* Prevents Server-Side Request Forgery where a malicious prompt could trick
* the agent into fetching internal resources like cloud metadata endpoints
* (169.254.169.254), localhost services, or private network hosts.
*
* Cloud metadata endpoints are ALWAYS blocked regardless of any toggle —
* they are never legitimate agent targets.
*
* Inspired by hermes-agent's `url_safety.py`.
*/
init_logger();
const log = createLogger("url-safety");
/** Hostnames that are always blocked (cloud metadata services). */
const BLOCKED_HOSTNAMES = new Set(["metadata.google.internal", "metadata.goog"]);
/** IPs that are always blocked regardless of any toggle. */
const ALWAYS_BLOCKED_IPS = new Set([
	"169.254.169.254",
	"169.254.170.2",
	"169.254.169.253",
	"100.100.100.200"
]);
function isBlockedIPv4(host) {
	const parts = host.split(".").map((p) => Number(p));
	if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
	const [octet1, octet2] = parts;
	if (octet1 === 10) return true;
	if (octet1 === 127) return true;
	if (octet1 === 0) return true;
	if (octet1 === 169 && octet2 === 254) return true;
	if (octet1 === 192 && octet2 === 168) return true;
	if (octet1 === 172 && octet2 >= 16 && octet2 <= 31) return true;
	if (octet1 === 100 && octet2 >= 64 && octet2 <= 127) return true;
	return false;
}
function isBlockedIPv6(host) {
	const normalized = host.toLowerCase().replace(/^\[|\]$/g, "");
	if (normalized === "::1") return true;
	if (normalized.startsWith("fe80:")) return true;
	if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
	return false;
}
/**
* Check whether a URL is safe to fetch (not targeting private/internal addresses).
*
* Blocks:
* - Cloud metadata endpoints (always, non-negotiable)
* - Private/loopback/link-local IP addresses
* - localhost and internal hostnames
* - URLs with embedded credentials
* - Non-HTTP(S) schemes
*
* Returns `{ safe: true }` for allowed URLs,
* or `{ safe: false, reason }` for blocked ones.
*/
function checkUrlSafety(rawUrl) {
	let url;
	try {
		url = new URL(rawUrl.trim());
	} catch {
		return {
			safe: false,
			reason: "Invalid URL"
		};
	}
	if (url.protocol !== "http:" && url.protocol !== "https:") return {
		safe: false,
		reason: "Only http and https URLs are allowed"
	};
	if (url.username || url.password) return {
		safe: false,
		reason: "URLs with embedded credentials are not allowed"
	};
	const rawHost = url.hostname.toLowerCase();
	if (!rawHost) return {
		safe: false,
		reason: "Missing hostname"
	};
	const host = rawHost.replace(/^\[|\]$/g, "");
	if (BLOCKED_HOSTNAMES.has(host)) {
		log.warn({ host }, "Blocked request to cloud metadata hostname");
		return {
			safe: false,
			reason: `Blocked: cloud metadata endpoint '${host}'`
		};
	}
	if (host === "localhost" || host.endsWith(".localhost")) return {
		safe: false,
		reason: "Blocked: localhost"
	};
	if (host.endsWith(".local") || host.endsWith(".internal")) return {
		safe: false,
		reason: "Blocked: internal hostname"
	};
	if (isIPv4(host)) {
		if (ALWAYS_BLOCKED_IPS.has(host)) {
			log.warn({ host }, "Blocked request to cloud metadata IP");
			return {
				safe: false,
				reason: `Blocked: cloud metadata address ${host}`
			};
		}
		if (isBlockedIPv4(host)) return {
			safe: false,
			reason: "Blocked: private or non-public IPv4 address"
		};
		return { safe: true };
	}
	if (isIPv6(host)) {
		if (isBlockedIPv6(host)) return {
			safe: false,
			reason: "Blocked: private or loopback IPv6 address"
		};
		return { safe: true };
	}
	return { safe: true };
}
/**
* Assert that a URL is safe to fetch. Throws on blocked URLs.
* Convenience wrapper over {@link checkUrlSafety}.
*/
function assertUrlSafe(rawUrl) {
	const result = checkUrlSafety(rawUrl);
	if (!result.safe) throw new Error(result.reason ?? "URL blocked by safety check");
}
/**
* Normalize a domain pattern for matching:
* strips protocol, path, www. prefix, trailing dots.
*/
function normalizeDomainPattern(raw) {
	let value = raw.trim().toLowerCase();
	if (!value || value.startsWith("#")) return void 0;
	if (value.includes("://")) try {
		value = new URL(value).hostname;
	} catch {
		value = value.split("://")[1] ?? value;
	}
	value = value.split("/")[0].split(":")[0].trim().replace(/\.+$/, "");
	if (value.startsWith("www.")) value = value.slice(4);
	return value || void 0;
}
function matchHostAgainstRule(host, pattern) {
	if (!host || !pattern) return false;
	if (pattern.startsWith("*.")) {
		const suffix = pattern.slice(2);
		return host === suffix || host.endsWith(`.${suffix}`);
	}
	return host === pattern || host.endsWith(`.${pattern}`);
}
/**
* Check whether a URL is blocked by the website blocklist.
*
* Returns `undefined` if allowed, or a block descriptor if blocked.
*/
function checkWebsiteBlocklist(rawUrl, blocklist) {
	if (!blocklist?.enabled || !blocklist.domains?.length) return void 0;
	let host;
	try {
		host = new URL(rawUrl.trim()).hostname.toLowerCase().replace(/\.+$/, "");
	} catch {
		return;
	}
	if (host.startsWith("www.")) host = host.slice(4);
	for (const rawRule of blocklist.domains) {
		const pattern = normalizeDomainPattern(rawRule);
		if (!pattern) continue;
		if (matchHostAgainstRule(host, pattern)) {
			log.info({
				host,
				rule: pattern
			}, "Blocked URL by website blocklist");
			return {
				host,
				rule: pattern,
				message: `Blocked by website policy: '${host}' matched rule '${pattern}'`
			};
		}
	}
}
/**
* Remove base64-encoded images from text to reduce token usage.
* Strips patterns like `data:image/png;base64,...` and their markdown wrappers.
*/
function cleanBase64Images(text) {
	let cleaned = text.replace(/!\[[^\]]*\]\(data:image\/[^;]+;base64,[A-Za-z0-9+/=]+\)/g, "[image removed]");
	cleaned = cleaned.replace(/\(data:image\/[^;]+;base64,[A-Za-z0-9+/=]+\)/g, "[image removed]");
	cleaned = cleaned.replace(/data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/g, "[image removed]");
	return cleaned;
}
//#endregion
export { assertUrlSafe, checkUrlSafety, checkWebsiteBlocklist, cleanBase64Images };

//# sourceMappingURL=url-safety.js.map