import { env } from "cloudflare:workers";
import { userContentOriginFor } from "./usercontent-origin";
import { bytesToHex, hexToBytes, hkdfSha256, hmacSha256, utf8 } from "./sync-hash";
export const CAMO_PREFIX = "/camo/";
const CAMO_SALT = "gitfor-camo";
const CAMO_INFO = "image-proxy-v1";
const MAX_URL_LENGTH = 2048;
/** One canonical spelling per URL, so case changes can't mint fresh cache keys. */
const LOWER_HEX = /^[0-9a-f]+$/;
let cachedKey: { secret: string; key: Uint8Array } | null = null;
/** MAC key derived from SECRETS_KEY with HKDF so the proxy never sees the master key directly. */
function camoKey(secret: string | undefined): Uint8Array | null {
if (!secret) return null;
if (!cachedKey || cachedKey.secret !== secret)
cachedKey = { secret, key: hkdfSha256(utf8(secret), CAMO_SALT, CAMO_INFO) };
return cachedKey.key;
}
/** Only absolute http(s) URLs without credentials are proxied; anything else is dropped by the caller. */
export function proxiableImageUrl(raw: string): string | null {
if (raw.length > MAX_URL_LENGTH) return null;
let url: URL;
try {
url = new URL(raw);
} catch {
return null;
}
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
if (url.username || url.password || !url.hostname) return null;
if (isOwnHost(url.hostname)) return null;
url.hash = "";
return url.href;
}
function hostOf(value: string | undefined | null): string | null {
if (!value) return null;
try {