export type MaybePromise = T | Promise import type { NodeResponseContext } from "@nifrajs/core/server" import { parseCookies as parseCoreCookies } from "@nifrajs/core/server" export const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]) const TEXT = new TextEncoder() export function jsonError( status: number, error: string, headers?: Record, ): Response { return Response.json( { ok: false, error }, headers === undefined ? { status } : { status, headers }, ) } /** Shared with core so auth/CSRF middleware gets the allocation-light cookie scanner too. */ export const parseCookies = parseCoreCookies export function quotedHeaderValue(value: string): string { let out = "" for (let i = 0; i < value.length; i++) { const c = value.charCodeAt(i) if (c < 0x20 || c === 0x7f) continue const ch = value[i] ?? "" out += ch === "\\" || ch === '"' ? `\\${ch}` : ch } return out } export function utf8Bytes(value: string): Uint8Array { return TEXT.encode(value) } export function secretBytes(secret: string | Uint8Array, label: string): Uint8Array { const bytes = typeof secret === "string" ? utf8Bytes(secret) : new Uint8Array(secret) if (bytes.byteLength < 32) { throw new Error(`${label}: secret must be at least 32 bytes`) } return bytes as Uint8Array } export async function sha256(input: string | Uint8Array): Promise> { const bytes = typeof input === "string" ? utf8Bytes(input) : new Uint8Array(input) return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)) as Uint8Array } export function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean { if (a.byteLength !== b.byteLength) return false let diff = 0 for (let i = 0; i < a.byteLength; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0) return diff === 0 } export async function timingSafeEqualString(a: string, b: string): Promise { const [left, right] = await Promise.all([sha256(a), sha256(b)]) return timingSafeEqualBytes(left, right) } export function base64UrlEncode(bytes: ArrayBuffer | Uint8Array): string { const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes) let bin = "" for (const b of view) bin += String.fromCharCode(b) return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") } export function base64UrlDecode(input: string): Uint8Array | null { if (!/^[A-Za-z0-9_-]*$/.test(input) || input.length % 4 === 1) return null const padded = input .replace(/-/g, "+") .replace(/_/g, "/") .padEnd(Math.ceil(input.length / 4) * 4, "=") try { const bin = atob(padded) const bytes = new Uint8Array(bin.length) for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) return bytes as Uint8Array } catch { return null } } export function decodeBase64(input: string): Uint8Array | null { try { const bin = atob(input) const bytes = new Uint8Array(bin.length) for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) return bytes as Uint8Array } catch { return null } } export async function importHmacKey( secret: string | Uint8Array, hash = "SHA-256", ): Promise { return crypto.subtle.importKey( "raw", secretBytes(secret, "hmac"), { name: "HMAC", hash }, false, ["sign", "verify"], ) } export async function hmacSha256(input: string, secret: string | Uint8Array): Promise { const key = await importHmacKey(secret, "SHA-256") const sig = await crypto.subtle.sign("HMAC", key, utf8Bytes(input)) return base64UrlEncode(sig) } export async function verifyHmacSha256( input: string, signature: string, secret: string | Uint8Array, ): Promise { const sig = base64UrlDecode(signature) if (sig === null) return false const key = await importHmacKey(secret, "SHA-256") return crypto.subtle.verify("HMAC", key, sig, utf8Bytes(input)) } /** * Apply header mutations to a response, in place when possible. Framework-built responses * (`new Response`, `Response.json`) have mutable headers across every runtime - so the common path * mutates `res` directly and returns it, allocating nothing. Only an *immutable*-headers response * (`Response.redirect()`/`Response.error()`, or a proxied `fetch()` response on Node/Deno/workerd - * never on Bun) makes `.set`/`.append` throw; that path clones into a fresh `Headers` + `Response`, * exactly the old always-clone behavior. `apply` runs once either way (immutability is all-or-nothing * - the first mutation throws before any partial change), so a mutation chain is safe to pass. */ export function withHeaders(res: Response, apply: (headers: Headers) => void): Response { const responseHeaders = res.headers const probe = "x-nifra-header-probe" let previous: string | null = null let mutable = true try { previous = responseHeaders.get(probe) responseHeaders.set(probe, "1") if (previous === null) responseHeaders.delete(probe) else responseHeaders.set(probe, previous) } catch { mutable = false try { if (previous === null) responseHeaders.delete(probe) else responseHeaders.set(probe, previous) } catch { // The guarded response rejected cleanup; the clone below is authoritative. } } if (!mutable) { const headers = new Headers(res.headers) apply(headers) return new Response(res.body, { status: res.status, statusText: res.statusText, headers }) } apply(responseHeaders) return res } /** * Set one Node-direct response header without constructing a Web Response. `name` must already be * the lowercase wire spelling - the record documents that contract, and it is what `Headers` emits * on every other runtime. * * The outcome's own record is used AS-IS. Re-homing it into a null-prototype object - the obvious * way to make `__proto__` inert - demotes it to V8's dictionary mode for the rest of the request, * and everything downstream then pays dictionary lookups on the response hot path: the direct * writer's key walk, and Node's own `_storeHeader` walk over every name. Values here are strings and * string arrays, so assigning one through the inherited `__proto__` setter is a spec-level no-op * rather than pollution; the single name that setter would swallow is defined as an own data * property instead. Same trade core's portable header view makes over the same record. */ export function setNodeHeader( res: NodeResponseContext, name: string, value: string | readonly string[], ): void { res.headers ??= {} const headers = res.headers if (name === "__proto__") { Object.defineProperty(headers, name, { value, writable: true, enumerable: true, configurable: true, }) return } headers[name] = value }