Phase 1: preview contracts and pure session core, 61 tests

src/lib/preview/ — framework-free ESM modules, per design §12–14:
- contracts.js: versioned manifest/item/source-ref factories; signed
  URLs structurally excluded from manifests; Apple identity first-class.
- lease.js: discriminated MediaLease union; computeSafeForMs derives
  lifetimes provider-clock-first with fallback/contradiction handling
  (margins are ADRs backed by Phase 0 soak evidence).
- redact.js: URL redaction (host+hash+exp only) and the HTTPS + exact
  host allowlist gate applied to initial URLs and every redirect.
- manifest.js: artist (rank order, exact-id dedupe), album (disc/track
  sort, context inheritance) and fixture builders — pure, no fetching.
- session.js: reducer with session/generation guards on every async
  action; keyed active item with derived playable indices/cursor;
  auto-advance under fresh generations; close is terminal.
- resolver.js: PreviewResolver contract + scripted fixture resolver
  with classified failures (stale_lease/not_found/network/…).
- transport.js: transport seam + deterministic fake (manual ticks).
- focus.js: revision gateway (all main-player entry points; focus-
  internal tag) and the coordinator — acquisition only via a
  generation-bound silence ack, restore only with revisions unchanged.

tests/preview/*.test.mjs run via `npm test` (node --test): 61 passing,
covering the full Phase 1 list in IMPLEMENTATION-PLAN §6 including the
exit gate: stale lease/play events are inert, no restoration after any
simulated main command, and no component renders anywhere.
This commit is contained in:
KeelrLeek committed 2026-07-22 18:29:03 -05:00
1 parent 6dcc62a2fd
commit 8ede6a8adf
14 files changed
+1722 -2

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
// URL redaction and provider host policy for the preview feature.
//
// Signed preview URLs are ephemeral secrets: they carry an hmac and expire in
// minutes (Phase 0: 13–15 min, then 403). They must never appear in logs,
// session persistence, or diagnostics. Everything that wants to talk about a
// URL goes through redactUrl() and gets host + a stable hash + the parsed
// expiry — enough to correlate, nothing to replay.
/** Providers the preview resolver may fetch media from. HTTPS only; exact
* host match — no subdomain wildcards, because the CDN host is a single
* stable name (Phase 0: 71/71 samples on cdnt-preview.dzcdn.net). */
export const MEDIA_HOST_ALLOWLIST = Object.freeze([
"cdnt-preview.dzcdn.net",
// Apple preview hosts land here in Phase 2 when Apple identity is
// plumbed through the backend; adding a host is a reviewed change.
]);
/** Parse the signed expiry out of a preview URL. Deezer uses an Akamai
* `hdnea=exp=<epoch-seconds>~…` token; a bare `exp=` query also matches.
* Returns epoch MILLISECONDS or null — never throws. */
export function parseSignedExpiryMs(url) {
if (typeof url !== "string") return null;
const m = /(?:^|[?&~])(?:hdnea=)?exp=(\d{9,11})(?:[~&]|$)/.exec(url);
if (!m) return null;
return Number(m[1]) * 1000;
}
/** FNV-1a — tiny, stable, dependency-free. Not cryptographic and does not
* need to be: the hash only correlates log lines about the same URL. */
function fnv1a(str) {
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return (h >>> 0).toString(16).padStart(8, "0");
}
/** The only representation of a media URL allowed outside the transport:
* `{ host, hash, expMs }`. */
export function redactUrl(url) {
if (typeof url !== "string" || url.length === 0) {
return { host: null, hash: null, expMs: null };
}
let host = null;
try {
host = new URL(url).host;
} catch {
/* malformed: host stays null, hash still correlates */
}
return { host, hash: fnv1a(url), expMs: parseSignedExpiryMs(url) };
}
/** True only for an https URL whose host is on the media allowlist. Used for
* the initial URL and re-checked for every redirect target — a redirect off
* the allowlist is a hard failure, not a follow. */
export function isAllowedMediaUrl(url) {
try {
const u = new URL(url);
return u.protocol === "https:" && MEDIA_HOST_ALLOWLIST.includes(u.host);
} catch {
return false;
}
}