import { randomBytes } from "node:crypto"; import QRCode from "qrcode"; import { env } from "@/lib/env"; /** * Operation QR tokens are opaque, URL-safe, high-entropy identifiers * printed on traveler cards. 24 bytes = 192 bits of entropy, encoded * as base64url -> 32 characters. */ export function generateQrToken(): string { return randomBytes(24) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } /** * The payload baked into the QR image is a full absolute URL so any phone * camera app (which opens scans in a browser) goes straight to the scan page. * APP_URL must match the externally reachable origin — see docs/DEPLOY.md. */ export function scanUrlForToken(token: string): string { const base = env.APP_URL.replace(/\/+$/, ""); return `${base}/op/scan/${token}`; } /** * Render the scan URL as a data-URL PNG suitable for on the * admin detail page and, later, on the printable traveler card. Error * correction level M balances density against smudge tolerance on paper. */ export async function renderQrPng(token: string): Promise { return QRCode.toDataURL(scanUrlForToken(token), { errorCorrectionLevel: "M", margin: 1, width: 256, }); } /** * Same QR, but as a raw PNG buffer for embedding into pdf-lib documents. * We bump width up so the vector→raster downscale at print time stays crisp; * 512 px at 35 mm = ~370 dpi which beats any office printer we'll see. */ export async function renderQrPngBuffer(token: string, width = 512): Promise { return QRCode.toBuffer(scanUrlForToken(token), { errorCorrectionLevel: "M", margin: 1, width, }); }