/** Pure USERCONTENT_ORIGIN validation and grant encoding (no Worker bindings), so it unit-tests under bun. */
import { bytesToHex, hexToBytes, hmacSha256, utf8 } from "./sync-hash";
export const USERCONTENT_GRANT_PARAM = "uc";
/** Long enough for a page full of images to finish loading, short enough that a leaked link goes stale fast. */
export const USERCONTENT_GRANT_TTL_MS = 15 * 60 * 1000;
const MAX_GRANT_PATH = 4096;
const MAX_GRANT_LENGTH = 256;
/** Read-only paths whose bodies are user-supplied bytes; uploads stay on the forge origin with its cookie and CSRF checks. */
const SERVE_PATHS = [
/^\/[^/]+\/[^/]+\/raw\/.+$/,
/^\/[^/]+\/[^/]+\/archive\/.+\.(?:tar\.gz|tgz)$/,
/^\/[^/]+\/[^/]+\/releases\/download\/[^/]+\/[^/]+$/,
/^\/attachments\/[a-z0-9]+\/[^/]+$/,
/^\/avatars\/(?:u\/)?[^/]+$/,
/^\/camo\/[^/]+\/[^/]+$/,
];
/** User-content paths that serve the same public bytes to everyone, so their redirects need no viewer grant. */
const PUBLIC_PATHS = [/^\/avatars\/(?:u\/)?[^/]+$/];
export function isPublicUserContentPath(pathname: string): boolean {
return PUBLIC_PATHS.some((pattern) => pattern.test(pathname));
}
export function isUserContentPath(pathname: string): boolean {
return SERVE_PATHS.some((pattern) => pattern.test(pathname));
}
function within(host: string, domain: string | undefined): boolean {
const base = (domain ?? "")
.trim()
.toLowerCase()
.replace(/^\.+|\.+$/g, "");
return base !== "" && (host === base || host.endsWith(`.${base}`));
}
/**