Release v0.2.0: Asset and Media Streaming (media by reference)#4
Merged
Conversation
…erence Plan 12-01 (Wave 1): verify shipped by-reference pipeline (relay byte-identity, confirm-only CSP img-src/no-media-src/no-script-src) + RED Wave-0 scaffolds. Plan 12-02 (Wave 2): capture ASSET_DATA_URI_MAX_BYTES + blob:/oversized-data placeholder degrade + clone-only data-ps-currentsrc + firing D26 oracle entry. Plan 12-03 (Wave 2): pure fail-closed origin classifier + mediaMode + pre-write fetch gate (string-layer snapshot + diff ADD/ATTR + subtree) + currentSrc pin + SECURITY.md. Covers ASST-01..05, MSEC-01, MSEC-02. security_enforcement: each plan carries a STRIDE threat_model; HIGH SSRF/tracking threats mitigated by the pre-write origin gate. Populated VALIDATION.md Per-Task Verification Map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- add tests/differential/fixtures/static-assets.html: focused asset-surface fixture (img/srcset/picture+source/svg-image/background-image/video-poster/ blob-img/oversized-data-img) for the oracle; oversized data: URI > 262144 B seeds Plan 02's degrade cap, blob: seeds the unfetchable degrade - add tests/relay-asset-bytes.test.js: SC#1 byte-identity proof -- a snapshot carrying absolute https image URL strings is forwarded receive->send verbatim (relay never fetches/parses/rewrites; no image bytes traverse the wire) - extend tests/renderer-snapshot.test.js: confirm-only CSP assertion (has img-src, has default-src 'none', NO script-src, NO media-src) -- pins the Phase-13 media-src deferral as intentional; CSP_META in snapshot.js unchanged
…cy, asset-gate - tests/capture-asset-degrade.test.js (ASST-03/04, filled by 12-02): 7 cases -- blob:/oversized-data:/small-data: classification, currentSrcDiffers true/false, srcset+injected-currentSrc enrichment, and the no-live-mutation invariant - tests/renderer-asset-policy.test.js (MSEC-01, filled by 12-03): 15 cases -- full https-only + private/internal-range denylist table (localhost/127/10/ 172.16-31/192.168/169.254/::1/fc00/fd + unqualified/.local) + 172/12 boundary + parse-error + one allowed public-https - tests/renderer-asset-gate.test.js (ASST-02/03/05, MSEC-01/02, filled by 12-03): 8 cases -- mediaMode off/poster/reference, blocked-origin->placeholder, currentSrc pin + srcset/sizes neutralize, throwing-hook fail-closed, allowlist RED contract: files parse cleanly; dynamic import() inside each test makes the missing exports/module surface as per-test FAILURES (not load/syntax errors). Intentionally RED until Plans 12-02/12-03 land; the parse-check verify passes.
- 12-01-SUMMARY.md: SC#1 relay byte-identity + confirm-only CSP pinned; asset fixture + 3 RED Wave-0 scaffolds (intentionally RED until 12-02/12-03 land) - STATE.md: advance to plan 2/3; record metric; log 12-01 decisions - ROADMAP.md: phase 12 plan progress 1/3 (In Progress) - REQUIREMENTS.md: mark ASST-01, ASST-02, ASST-05 complete
…rentSrc pin - add ASSET_DATA_URI_MAX_BYTES (256 KiB) to src/protocol/constants.js with units/derivation comment - add pure classifyAssetRef(url, capBytes): blob:/oversized-data: -> non-shareable, small data:/http(s) -> ok - add pure currentSrcDiffers(currentSrc, src): clone-only data-ps-currentsrc enrich predicate - add createAssetUnavailablePlaceholder + replaceWithAssetUnavailablePlaceholder (modeled on createBlockPlaceholder) - hook degrade + clone-only currentSrc enrich at all 4 serialization sites (snapshot pair walk, iframe-content loop, added-node root+descendants wireClone, mutation attr branch) - export serializeSnapshot(doc) one-shot helper (no-observer path for unit tests) - live DOM is never mutated by the enrichment/degrade (clone-only; Pitfall 4) - tests/capture-asset-degrade.test.js GREEN (7/7); existing oracle unaffected (no over-degrade, Pitfall 5)
…grade ledger entry - add tests/differential/scenarios/static-assets.js: injects a divergent currentSrc (jsdom currentSrc==='') so the clone-only enrichment fires - add D26-currentsrc-variant-pin mismatch entry (combined ASST-03 pin + ASST-04 degrade) with htmlContainsCurrentSrcPin / htmlContainsAssetUnavailable predicates - register static-assets.html MATRIX row + scenario import in oracle.test.js - add static-assets per-scenario assertion block (D26 fires exactly once; pin present in ext/absent in ref; blob/oversized degrade to placeholder; blob: never on the ext wire; small data: byte-identical, Pitfall 5) - add static-assets EMPTY-ledger load-bearing test - D26 alone covers the single same-index SNAPSHOT mismatch; D27 NOT added (a second mismatch entry could never fire and would fail stale-entry detection) - oracle.test.js GREEN (44/44); D26 matched, stale-entry detector satisfied
…-01)
- New src/renderer/asset-policy.js: classifyAssetOrigin(url) + isPrivateOrLocalHost(host),
pure (DOM-free, network-free), named exports, Phase-15-reusable
- https-only fetch gate; denies localhost/127.0.0.0-8/10.0.0.0-8/172.16.0.0-12
(incl. /12 boundary)/192.168.0.0-16/169.254.0.0-16/::1/fc00::-7/.local/unqualified;
any parse error fails closed -> { allowed:false, reason }
- Strips WHATWG-retained IPv6 brackets before range checks; .local routes to
unqualified-host (not private-host) per the table contract
- Fills tests/renderer-asset-policy.test.js (15) to GREEN; purity scan stays green
(no allow-scripts literal in the new module)
…tSrc pin (MSEC-01/MSEC-02/ASST-03) - Module-level gateAssetUrl(url, ctx) pure fail-closed orchestrator: mediaMode off blocks all; allowAssetOrigins host-widen; classifyAssetOrigin deny authoritative; assetOriginPolicy hook fail-closed (throw OR non-true blocks); poster passes under poster mode (full split is Phase 13) - createViewer reads mediaMode (default reference; invalid value throws at factory time -- the sanctioned throw site), assetOriginPolicy, allowAssetOrigins; accepts mount alias + optional transport (host-driven API) and exposes handleSnapshot - snapshot.js gateSnapshotAssets(): STRING-layer pre-srcdoc gate (Pitfall 1) rewrites blocked <img> to a dimensioned data-ps-asset-unavailable=blocked-origin placeholder and applies the ASST-03 currentSrc pin (effective src = data-ps-currentsrc, srcset/ sizes neutralized). Pure string rewrite of emitted values -- NOT scrub-then-reparse; no innerHTML sink added; CSP_META byte-unchanged - DOM-fragment gate wired pre-write at diff ADD (inert template), diff ATTR (pre- setAttribute for src/poster), subtree-response (pre-importNode), and the post-parse scrub as defense-in-depth; gate hooks injected into applyMutations - Placeholders carry no live identity attr (positional nid pairing preserved -- Phase 7); sandbox token + CSP unchanged, no allow-scripts literal - Fills tests/renderer-asset-gate.test.js (8) to GREEN; extends the two handle-surface lock tests for the new host-driven handleSnapshot entry point; full npm test 449/449
…MSEC-02) - New SECURITY.md section 6 'Viewer-side resource fetching': v2.0 verb change render-inert -> fetch; fail-closed https-only origin policy + full private/internal denylist (localhost/127/10/172.16.0.0-12/192.168/169.254/::1/fc00::-7/.local); assetOriginPolicy fail-closed hook + allowAssetOrigins; mediaMode off|poster|reference (default reference, P12 poster-scope note); the pre-write string-layer timing rule; currentSrc pin; sandbox + CSP unchanged (no script-src, no media-src -- Phase 13) - Adds an asset must-never (no allow-by-default, no post-write gate); renumbers Residual Risks to section 7 - All pinned chokepoint-purity markers preserved; adds img-src/Viewer-side-fetch/mediaMode markers to the purity test so the asset CSP surface cannot silently regress - Full npm test 449/449 green
- 12-03-SUMMARY.md: viewer-side-fetch security model (classifyAssetOrigin + gateAssetUrl pre-write gate at all 4 sites + mediaMode + currentSrc pin); 3 deviations documented; Playwright asset UAT deferred; self-check PASSED - STATE.md: Phase 12 COMPLETE (3/3); 12-03 decisions + metric row - ROADMAP.md: phase 12 3/3 plans, status Complete - REQUIREMENTS.md: MSEC-01/MSEC-02 marked complete (ASST-02/03 already complete)
…local/NAT64/trailing-dot hosts
…SEC-04) - add <meta name="referrer" content="no-referrer"> at BOTH buildSnapshotHtml- family return sites, immediately after CSP_META and before <meta charset> - one document-level control covers every viewer-side fetch (img/video/source/ poster/background-image) so the mirrored page URL never leaks in a Referer header to third-party origins - no crossorigin added: allow-same-origin sandbox + no crossorigin already omits credentials; forcing crossorigin would break non-CORS assets (locked decision) - CSP_META byte-unchanged (default-src 'none', media-src blob:, no script/connect) - update renderer-snapshot CSP-first pin to <head>+CSP+referrer+charset prefix
…a 3-4)
- New tests/security-media.test.js: 4 named invariants backing the Plan-04 object-URL threat model
- media-player.js zero allow-scripts outside comments (sandbox-token over media code; threat rows 1-2) -- reuses purity stripComments+countMatches, does NOT edit the purity glob (Pitfall 6)
- deps byte-unchanged gate: dependencies === { ws: '8.21.0' } + peerDependenciesMeta['hls.js'].optional (threat T-15-13)
- media-sync security: late cross-session STREAM.MEDIA rejected by isCurrentStream, no driver (cites renderer-media.test.js:411; threat T-15-11)
- parent-realm object URL revoked on destroy/destroyAll (cites renderer-media-player.test.js revoke coverage; threat T-15-12)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 15-03-SUMMARY.md: 4 named invariants backing the Plan-04 object-URL threat model; Wave-2 gate result recorded - STATE.md: advance to plan 4 of 4 (status stays executing -- 15-04 docs remains), metric + decision recorded - ROADMAP.md: phase 15 plan progress (3/4 summaries, In Progress) - Gate: full suite 700/700, differential oracle 48/48 no new ledger entry, package-publish 6/6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tials completed, object-URL threat model - Add §4 asset/media URL masking vocabulary (maskMediaSelector, maskAssetUrls, maskAssetUrlFn) + the token/PII query-param denylist table (AWS SigV4/SigV2, GCP, Azure SAS, generic; case-insensitive exact-name OR x-amz-/x-goog- prefix) - Add §6 Referrer and credentials subsection: document-level <meta name=referrer content=no-referrer> after CSP + no-crossorigin omit-credentials posture; mark Phase-15 masking/referrerpolicy completed (line-214 forward-reference now past tense) - Add Parent-Realm Object-URL Threat Model subsection (5 STRIDE rows + plain-language worst case: child plays but cannot script/read/exfiltrate the blob) with backing-test cites - Extend purity-test requiredMarkers: keep all 12 existing verbatim + add maskMediaSelector/maskAssetUrls/maskAssetUrlFn/referrer/no-referrer/Parent-Realm Object-URL; rendererModules() glob and the element/subtree/attr/text/css dispatch list unchanged
- Rewrite limitation #6: <video>/<audio> now mirrored by reference across v2.0 (playback state + progressive + best-effort adaptive HLS/DASH from a parent-realm surface; bytes never cross the relay) - Narrow residual media limits to DRM/EME (poster), MSE/blob without a discoverable manifest (poster), and raw media pixels/frames (out of scope — by-reference not by-pixel) - Keep closed shadow roots + cross-origin iframe content as the standing browser-boundary limits
- 15-04-SUMMARY.md: MSEC-04 security-contract docs (SECURITY.md §4 masking vocab + denylist, §6 referrer/no-credentials completed, Parent-Realm Object-URL Threat Model; ARCHITECTURE.md limitation #6 media-by-reference); 12 purity markers kept verbatim + 6 added in the same change; suite 700/700 - STATE.md: Phase 15 COMPLETE (4/4), progress 73/73 plans / 93%, status verifying, 15-04 metric + decision recorded - ROADMAP.md: Phase 15 + 15-04-PLAN marked complete; progress row 4/4 Complete (2026-06-21) - Final v2.0 phase complete -- ready for verification
…, not token-masked)
…N (contracts satisfied, suite green)
…n sound, deferred real-browser UAT)
…+ Phase 15 CSP/referrer/sanitization PASS live; media items remain hidden-tab-deferred
…LIVE (autoplay, drift-sync+hard-seek, unmute affordance)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a5ecd1b57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LakshmanTurlapati
added a commit
that referenced
this pull request
Jun 21, 2026
Codex flagged on PR #4 that the pre-srcdoc string-layer gate (gateOneMediaTag) reads only a `src` attribute, so a responsive <picture><source srcset="https://169.254.169.254/x.png 2x"> re-emits the blocked candidate unchanged. A real browser prefetches srcset candidates during srcdoc parse, before the post-parse DOM pass can sanitize, so this was a live SSRF / fetch-policy bypass. Gate <source srcset> with the same per-candidate vocabulary the <img> path already uses (srcsetHasBlockedCandidate): if any candidate is blocked and no allowed src remains, fall back to the dimensioned placeholder; otherwise strip only the offending srcset so the allowed src can still fetch. <video>/<audio> carry no srcset, so this is scoped to <source>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LakshmanTurlapati
added a commit
that referenced
this pull request
Jun 21, 2026
Codex flagged on PR #4 that the string-layer opener scanner enumerated only <img>/<video>/<source>, so an <audio src="..."> was never gated before srcdoc parse. A real browser starts the media GET during parse (a private-origin SSRF, or full media bytes in off/poster mode) before the post-parse gateFragmentMedia pass can strip it. Add <audio> to the opener scan; gateOneMediaTag already handles any tag name uniformly (audio src gates as 'media'; the video-only poster branch is naturally skipped). Generalize the container-orphan guard, which was hardcoded to <video>, so a neutralized <audio> also consumes its matching </audio> close tag and swallowed children rather than leaving an orphan in the stream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LakshmanTurlapati
added a commit
that referenced
this pull request
Jun 21, 2026
Codex flagged on PR #4 that the diff ATTR gate always gated a `src` mutation as kind 'image'. The per-viewer gate closure binds mediaMode, so a poster-mode `video.src = <public-https>` (or `source.src`) mutation passed as 'image' never tripped the poster-mode-media deny and was written to the live mirror, fetching playable media bytes that poster mode exists to withhold. Pick the gate kind from the live element's tag (reusing the existing lowercased targetTag): <video>/<audio> src -> 'media', <source> src -> 'source', any other src -> 'image'; a poster attr stays 'poster'. This mirrors gateOneMediaTag and gateFragmentMedia, so a poster-mode media src mutation is now denied and dropped, while the <img> path is intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LakshmanTurlapati
added a commit
that referenced
this pull request
Jun 21, 2026
fix(renderer): close 3 Codex security findings from PR #4 (media gate)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This release extends PhantomStream beyond DOM and text to mirror media by reference. The viewer now renders images, video, and audio by loading the original source URL over its own network, while the relay continues to carry only text and small state messages. The low bandwidth core value is preserved because media bytes never traverse the relay.
The work landed across the four phases of the v2.0 milestone, Asset and Media Streaming. Every phase ends runnable and green against
node --testand the differential oracle.What is new
Static assets by reference (Phase 12)
Images, srcset, picture, source, SVG image, CSS background image, and video poster all resolve to absolute source URLs and render in the viewer by fetching from the origin. The displayed variant is pinned, and non shareable references degrade to a dimensioned placeholder. A fail closed viewer fetch security model is established: an https only origin policy that blocks private and internal ranges, a
mediaModeswitch (off, poster, reference), and a precise Content Security Policy.Progressive video and audio with playback sync (Phase 13)
Progressive video (mp4, webm) and audio (mp3, ogg) play in the viewer from the source URL, driven cross realm from the parent so that no player code runs inside the sandbox. Playback state streams over a new throttled
STREAM.MEDIAside channel and applies with a pure, drift corrected reconciler. Small drift is held within tolerance, large drift triggers a hard seek, and live streams rejoin the edge. The viewer honors browser autoplay policy with a muted autoplay default and an observable affordance when playback is blocked.This capability was validated live in real Chrome. The mirror video autoplayed muted from the source URL, held lockstep sync during playback, hard seeked back to within 0.02 seconds after a deliberate 25 second source jump, and surfaced the unmute affordance on a muted mismatch.
Adaptive streaming with discovery and fallback (Phase 14)
When an HLS or DASH manifest is available, the viewer plays it through an optional, lazy player that runs in a renderer owned parent realm and binds a MediaSource object URL cross realm to the inert in iframe element. The Playwright and extension adapters can surface manifest URLs by network observation as opt in hints with graceful absence. Media that cannot be referenced, such as MSE without a manifest or DRM, degrades to a poster with an observable reason, so the mirror never breaks. The published module stays free of hard runtime dependencies, because hls.js is declared only as an optional peer dependency and is loaded through a dynamic import.
Media security, masking, threat model, and docs (Phase 15)
The security contract threaded through the earlier phases is completed. This adds an asset and media URL masking vocabulary that strips token and PII query parameters and omits private media URLs from the wire, a document level
referrerpolicy="no-referrer"with no credentials by default, a threat model of the parent realm object URL blast radius, media specific security tests, and updates todocs/SECURITY.mdanddocs/ARCHITECTURE.md.Quality and verification
All 20 milestone requirements are satisfied. Cross phase integration was traced through the code and confirmed sound. The full test suite passes at 704 of 704. The differential oracle stays at 48 of 48, which confirms the capture wire is byte identical. No new hard dependencies were added. The sandbox stays exactly
allow-same-origin, and the envelope and relay are unchanged.A real Chrome pass through the FSB browser also validated the asset pipeline and the security posture live. This included the by reference image render, the blocked origin placeholder, script blocking, the no referrer and CSP metas, and on event handler stripping, in addition to the Phase 13 playback validation noted above. The residual real browser checks for the adaptive MSE path are tracked in the per phase HUMAN-UAT files and are best run through the Playwright adapter demo.
Versioning
This is a minor release, from 0.1.0 to 0.2.0. Every addition is backward compatible by design. Old viewers ignore the new wire types, the envelope and relay are untouched, and no public API breaks. The version is a minor bump rather than a major bump for that reason, even though it ships the v2.0 milestone feature set.
Publishing
Publishing runs through the
publish.ymlworkflow when a GitHub Release is published, using npm trusted publishing rather than a stored token. After this PR merges, creating a release for 0.2.0 will runpackage:checkand publish@full-self-browsing/phantom-stream@0.2.0to the public registry.Generated with Claude Code