Files
EpicNext-Cms/src/lib/imager-http.ts
T
openhands f3407ebfdf
CI / check (push) Successful in 37s
CI / tests-integration (push) Successful in 2m2s
CI / tests-unit (push) Successful in 2m9s
CI / tests-ui (push) Successful in 2m44s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m12s
fix(imaging): support .hab assets, cache avatars/badges, harden routes
The hotel moved from .nitro to .hab bundles, so the imager must treat .hab
like .nitro and the public avatar endpoints must accept the figure strings
the client sends (including extra trailing segments such as `.0`, which the
old FIGURE_RE rejected with a 400).

- imager-upstream: relax FIGURE_RE, extend parseAvatarQuery test coverage
- api/imaging/avatar + /imaging: shared validation, placeholder and ETag/304
- api/imaging/badge: single-flight upstream fetch and conditional responses
- imager-http/imager-png: strong ETag helpers and APNG passthrough guard
- csp + next/image: unoptimized external images, tightened CSP
- deps: override postcss-selector-parser to ^7.1.6
- also fold in the pending drizzle draft snapshot and stray test cleanup
2026-10-14 20:40:50 +02:00

34 lines
1.1 KiB
TypeScript

import "server-only";
import { createHash } from "node:crypto";
/**
* Strong HTTP validator for an imaging response body.
*
* Derived from the bytes rather than the request URL on purpose: the same URL
* can legitimately return different bytes over its lifetime — a degraded
* fallback render before the configured imager recovers, or a different
* transcode variant — and a URL-based validator would let a browser keep the
* worse image via a stale `304`. Hashing the body means the ETag changes
* exactly when the bytes do.
*/
export function strongEtag(body: Uint8Array): string {
return `"${createHash("sha1").update(body).digest("base64url")}"`;
}
/**
* Whether an `If-None-Match` request header matches `etag`. Handles the
* comma-separated list form, weak validators (`W/"…"`) and the `*` wildcard, so
* a revalidating browser gets a `304` whenever its cached copy still matches.
*/
export function ifNoneMatchSatisfied(
header: string | null,
etag: string,
): boolean {
if (!header) return false;
return header.split(",").some((raw) => {
const tag = raw.trim();
return tag === "*" || tag === etag || tag === `W/${etag}`;
});
}