import 'server-only'; import { createHmac } from 'node:crypto'; import { IMAGE_WIDTHS, imageKey, type ImageWidth } from './images-core'; /** * Server-side helpers that turn a third-party image URL into signed `/img/…` proxy URLs. * Signing (HMAC over the URL with SESSION_SECRET) means only URLs the server chose to render * can be proxied — never arbitrary hosts. */ function secret(): string { return process.env.SESSION_SECRET ?? 'dev-only-session-secret-change-me-please'; } export function signImageUrl(url: string): string { return createHmac('sha256', secret()).update(`img:${url}`).digest('base64url').slice(0, 20); } export function verifyImageSignature(url: string, sig: string): boolean { return sig.length >= 20 && signImageUrl(url) === sig; } function b64url(s: string): string { return Buffer.from(s, 'utf8').toString('base64url'); } export function imgSrc(url: string, width: ImageWidth | number = 384, fmt: 'webp' | 'avif' = 'webp'): string { const key = imageKey(url); return `/img/${key}.${fmt}?w=${width}&u=${b64url(url)}&s=${signImageUrl(url)}`; } export interface ImageProps { src: string; srcSet: string; } /** `src` + `srcSet` for a responsive ``; `maxWidth` trims useless large variants for thumbnails. */ export function imageProps(url: string | null | undefined, opts: { maxWidth?: number } = {}): ImageProps | null { if (!url || !/^https?:\/\//i.test(url)) return null; const max = opts.maxWidth ?? 1200; const widths = IMAGE_WIDTHS.filter((w) => w <= Math.max(96, max * 2)); const list = widths.length ? widths : [IMAGE_WIDTHS[0]]; return { src: imgSrc(url, list[Math.min(list.length - 1, Math.max(0, list.findIndex((w) => w >= max)))] ?? 384), srcSet: list.map((w) => `${imgSrc(url, w)} ${w}w`).join(', '), }; }