feat: offline asset ledger and vendor preflight CLI - #3757
Conversation
- @hyperframes/core/asset-ledger: static asset-graph builder classifying every declared script/stylesheet/font/image/audio/video/iframe/track reference as remote | local | data | missing (no browser, no network) - hyperframes ledger [dir] --json --strict-offline: agent-readable inventory; non-zero exit when remote refs remain under strict mode - hyperframes vendor [dir] --out assets/vendor: downloads http(s) remotes, rewrites HTML references to relative paths, writes a provenance manifest - lint: remote_script_not_vendored info rule nudging toward vendoring
| return value | ||
| .replace(/&/gi, "&") |
| const FONT_FACE_BLOCK_RE = /@font-face\s*\{[^}]*\}/gi; | ||
|
|
||
| function stripCssComments(css: string): string { | ||
| return css.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)); |
| const { body, contentType } = await downloadAsset(fetchUrl, timeoutMs); | ||
| const fileName = vendorFileName(fetchUrl, contentType); | ||
| const vendorPath = posix.join(outRel, fileName); | ||
| writeFileSync(join(outDir, fileName), body); |
- ledger: single-pass HTML entity decoder so entities decode exactly once (no double-unescape; &lt; -> <, never <) - ledger: replace regex CSS comment stripping with a linear O(n) scanner (no backtracking on adversarial input) - vendor: path traversal guard pinning every write under the vendor dir, --out containment check under the project root, per-download size cap (--max-bytes, default 100 MB), and a comment documenting that the network-to-file write is the command's intended purpose - tests for single-decode semantics, comment stripping, traversal guard, size cap, and --out containment
|
Pushed fixes for the CodeQL findings (0a2710a): Double-unescape in ReDoS risk in Network→file in
No repo-idiomatic CodeQL suppression exists, so no suppression comment was added — just real guards plus the intent comment. Tests: |
| // (vendorFileName), a traversal check pinning every write under the | ||
| // vendor directory (resolveVendorTarget, with outDir itself pinned | ||
| // under the project root), and a per-download size cap. | ||
| writeFileSync(resolveVendorTarget(outDir, fileName), body); |
Summary
Agents and authors routinely pull CDN scripts and remote media into compositions, which silently breaks deterministic and offline renders. This PR adds an agent-readable asset graph and a one-command localization step:
hyperframes ledger [dir] [--json] [--strict-offline]— static inventory of every declared asset reference (scripts, stylesheets, fonts, images, audio, video, iframes, text tracks) across all project HTML, each classifiedremote | local | data | missing. No browser, no network. Under--strict-offlineit exits non-zero while any remote reference remains — the CI gate for deterministic renders.hyperframes vendor [dir] [--out assets/vendor] [--dry-run] [--strict-offline]— downloads each unique remote URL (scheme allowlist:http/httpsonly; protocol-relative upgraded tohttps), saves it as<basename>-<sha256-prefix><ext>, rewrites every HTML reference to a path relative to the declaring file, and records provenance (url,bytes,sha256,contentType) invendor-manifest.json. Remote iframes are deliberately never vendored — a live page cannot be made deterministic — so they still fail the strict gate.@hyperframes/core/asset-ledger— the pure extraction/classification engine (linear tag scan + CSSurl()/@import/@font-facescanning), reusing@hyperframes/parsers/asset-resolutionfor on-disk resolution. New subpath export synced viapackage-subpaths.json.remote_script_not_vendored(info) — nudges towardhyperframes vendorwhen a remote<script src>is detected, without breaking the documented CDN quick-start path.reference/cli-ledger(flags, JSON schema, exit codes) andguides/offline-deterministic-renders(workflow guide), both wired into the Mintlify nav.Scope note: this does not implement Google Fonts crawling (#3583) — only declared URLs are inventoried/vendored; font binaries nested inside remote CSS are documented as out of scope.
Walkthrough video (rendered by HyperFrames itself)
The walkthrough is not a screen recording — it is a HyperFrames HTML composition, storyboarded first and rendered to MP4 by the CLI. Every terminal/JSON panel shows the real captured output of running the new commands on the demo project, and the demo is self-referential: the composition was authored with GSAP on jsDelivr,
hyperframes vendordownloaded it toassets/vendor/gsap.min-0274614511.jsand rewrote the<script src>,hyperframes ledger --strict-offlinepassed with zero remote references, and the video was then rendered with the vendored file.▶ Walkthrough MP4 (public): https://raw.githubusercontent.com/mvanhorn/hyperframes/walkthrough-assets/offline-asset-ledger-walkthrough.mp4
(1920×1080, 30 s @ 30 fps, 2.2 MB — HyperFrames CLI render output)
Render command:
npx hyperframes render # → renders/offline-ledger-demo_2026-09-08_01-32-43.mp4 (rendered in 32.9s)Full demo pipeline (all real, exit codes verified):
Test plan
packages/core/src/assets/ledger.test.ts— 13 tests: URL classification (remote/data/local/skip, unknown schemes, templating placeholders), extraction (script tags,link rel/as, CSS@import/@font-face/backgrounds,srcset,poster,<source>parent-kind mapping, entity decoding, comment/script-string immunity), ledger assembly (counts, dedupe, resolver), and on-disk project scanning with file-relative resolution. Fixture HTML uses jsDelivr + local media; no browser required.packages/cli/src/commands/ledger.test.ts— 5 tests:--jsonoutput shape,--strict-offlineexit codes (1 with remotes, 0 when fully local), human output, error paths.packages/cli/src/commands/vendor.test.ts— 10 tests: scheme allowlist, stable hashed filenames, content-type extension fallback, relative-path rewriting (incl. prefix-safe longest-first replacement), full command flow with mockedfetch(download + rewrite + manifest),--dry-runno-op guarantee, failed-download exit 1,--strict-offlinefailure on non-vendorable remotes.packages/lint/src/rules/runtimeNetworkFetch.test.ts— 3 tests: info finding with vendor fixHint, URL dedupe, silence on local/inline scripts.@hyperframes/cli(vitest, exit 0),@hyperframes/core(exit 0),@hyperframes/lint(604 tests, exit 0).bun run build, rootbun run lint(incl.check:package-subpaths), typecheck, oxlint/oxfmt, and the fallow audit all pass.AI disclosure
This PR was implemented by an AI agent (Claude Fable 5, running as a Cursor cloud agent), including the code, tests, docs, storyboard, and the HyperFrames composition used for the walkthrough video. A human (mvanhorn) directed scope and requirements.