Files
iptvnator/tools/release/capture-tmdb-check.ts
4grayandClaude Opus 5 b4ec68c1fa feat(release): manifest-driven screenshot capture with fail-closed mock-data guards (#1261)
Third slice of the release-notes pipeline (#1256 format+generator, #1257 CI
gate): release screenshots become reproducible and provably mock-only.

The v0.20 capture script was single-use (hard-coded slugs, paths, hero) and
fail-open: a lost IPTVNATOR_E2E_DATA_DIR silently fell back to the user's
real ~/.iptvnator database, `...process.env` leaked ambient TMDB keys and
proxies, nothing gated network access, and no frame content was ever
validated. Each hole leaks real playlists, credentials, or copyrighted
artwork into published screenshots without a single signal.

New pipeline:

- tools/release/screenshots.manifest.json — declarative shots (slug, title,
  named setup steps, themes). Adding a feature shot = one manifest entry.
- capture-release-screenshots.ts — orchestrator; output goes to
  apps/website/public/blog/<release>/screenshots/<slug>-<theme>.png, release
  slug derived from package.json (or --release), --only/--theme filters.
- capture-app-driver.ts / capture-navigation.ts — launch, seeding, theme,
  and the named-action vocabulary; actions are order-independent (every
  portal action starts from the dashboard).
- screenshot-guards.mjs — the fail-closed policy, pure and unit-tested:
  G1 the real database is snapshotted (sha256+mtime) before launch and must
     be byte-identical after; the isolated DB must actually exist
  G2 the app receives an allowlisted environment, never ...process.env
  G3 deny-by-default network gate; known app-level calls (GitHub update
     check) are answered by local stubs; any other blocked request fails
     the run — a silently-blocked TMDB call would leave a frame that looks
     broken rather than unsafe
  G4 every frame is scanned before capture: external img/background URLs,
     credential-shaped text, MAC addresses, non-localhost m3u8 references
  G5 TMDB enrichment asserted disabled via the renderer's IndexedDB
  Any violation deletes every frame captured in the run and exits non-zero.

The guards paid for themselves on the first live run: G3 caught the mock
server redirecting stream endpoints to a public demo HLS
(test-streams.mux.dev) — meaning earlier hand-run captures could embed
third-party video frames. The M3U shot now deliberately captures the groups
layout without starting playback.

`.changes` validation now cross-checks `screenshot:` slugs against the
manifest, so a note cannot reference an image the capture run never
produces.

Verified end-to-end: 10/10 shots (5 slugs × dark/light) captured against
dist build + xtream-mock-server, frames visually inspected (fictional
titles/artwork only), guard-violation paths exercised live. 67 unit tests
in release-tools, lint green, script files within the repo size limit.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:19:02 +02:00

57 lines
2.0 KiB
TypeScript

/**
* G5: proof that TMDB enrichment stays off for the capture profile, so no
* licensed poster or still can reach a published screenshot.
*/
import type { Page } from '@playwright/test';
/**
* G5: settings live in the renderer's IndexedDB (ngx-pwa StorageMap). A
* fresh profile has no entry, which means the TMDB defaults (disabled,
* empty key) apply. Read-only assertion — if a future change flips the
* default or seeds a key, the run stops before a licensed poster can render.
*/
export async function assertTmdbDisabled(page: Page): Promise<void> {
const tmdb = await page.evaluate(
() =>
new Promise<{ enabled?: boolean; apiKey?: string } | null>(
(resolve) => {
const request = indexedDB.open('ngStorage');
request.onerror = () => resolve(null);
request.onsuccess = () => {
const db = request.result;
if (!db.objectStoreNames.contains('localStorage')) {
resolve(null);
return;
}
const get = db
.transaction('localStorage')
.objectStore('localStorage')
.get('settings');
get.onerror = () => resolve(null);
get.onsuccess = () =>
resolve(
(get.result as { tmdb?: { enabled?: boolean; apiKey?: string } })
?.tmdb ?? null
);
};
}
)
);
assertSync(
!tmdb?.enabled && !tmdb?.apiKey,
`G5 failed: TMDB enrichment is active in the capture profile (${JSON.stringify(tmdb)})`
);
}
function assertSync(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}