Refactor browser compatibility and restore CI/E2E coverage - #2
Refactor browser compatibility and restore CI/E2E coverage#2lemon-mint wants to merge 11 commits into
Conversation
|
Important Review skippedToo many files! This PR contains 167 files, which is 67 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (13)
📒 Files selected for processing (167)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoRestore browser contract fidelity and full CI/E2E coverage
AI Description
Diagram
High-Level Assessment
Files changed (41)
|
Code Review by Qodo
1. Silent peers leave sockets open forever
|
| const childDynamicEval = function dynamicEval(value) { | ||
| return typeof value === 'string' ? childExecGlobal(pageRewriteHooks.rewrite(value, 'eval')) : value; | ||
| }; | ||
| childFunctionFacade = function Function(...args) { |
There was a problem hiding this comment.
1. Reviewers cannot verify sandbox changes 📘 Rule violation ⛨ Security
installFrameRuntime replaces child-realm eval and Function with newly built rewrite gates without the mandated PHASE2 matrix reference. Dynamic code in srcdoc children and constructor-chain compilation now cross this sandbox boundary, while the PR description and changed comments name neither the matrix nor its required plan path.
Agent Prompt
## Issue description
Document this sandbox-boundary change's cross-verification against the `PHASE2 plan E1 escape matrix`, including the required plan path.
## Issue Context
The new child-realm dynamic compilation gates affect execution isolation and therefore require the explicit review reference mandated by the compliance checklist.
## Fix Focus Areas
- web/runtime-prelude.js[7628-7647]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const method = String(opt.method || (opt.request && opt.request.method) || 'GET'); | ||
| let body = null; | ||
| if (method !== 'GET' && method !== 'HEAD') { | ||
| try { |
There was a problem hiding this comment.
2. Request policy splits across runtimes 📘 Rule violation ⌂ Architecture
transportFetch now normalizes request bodies and decides same-origin mode, referrer, credentials, and origin-header policy directly in web/sw.js rather than delegating these decisions to Rust-WASM. Every runtime fetch passes through this branch, so browser request semantics and cross-origin policy are implemented in the service-worker layer alongside its WASM plumbing.
Agent Prompt
## Issue description
Move the newly added request-policy decisions and reusable transformations out of the service-worker JavaScript and into the Rust-WASM core.
## Issue Context
JavaScript should marshal browser requests and responses, while Rust-WASM owns method, body, credentials, referrer, origin, and redirect policy.
## Fix Focus Areas
- web/sw.js[1455-1495]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| test('runtime virtualizes dangerous sandbox attribute combinations on iframes', () => { | ||
| const rt = fs.readFileSync('web/runtime-prelude.js', 'utf8').split('\r\n').join('\n'); | ||
| assert.match(rt, /const\s+frameSandboxMeta\s*=\s*new\s+WeakMap\(\)/); | ||
| assert.match(rt, /function\s+frameSandboxAllowsEscape\(/); |
There was a problem hiding this comment.
3. Sandbox behavior loses test coverage 📘 Rule violation ▣ Testability
static-policy.test.js deletes the only test that checks dangerous iframe sandbox combinations are virtualized while the production frameSandboxMeta and frameSandboxAllowsEscape paths remain. No replacement test mentions allow-same-origin or those helpers, so regressions in attribute hooks from setAttribute through insertion are no longer caught.
Agent Prompt
## Issue description
Restore or replace the deleted regression test for dangerous iframe sandbox attribute virtualization.
## Issue Context
The production behavior remains active and security-sensitive, but its assertions covering the escape-token combination and related attribute hooks were removed without equivalent coverage.
## Fix Focus Areas
- test/js/static-policy.test.js[991-1004]
- web/runtime-prelude.js[4810-4832]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Do not send transport FIN after a Close: the relay would tear down | ||
| // both directions before the peer can echo. WriterPoll waits for it. |
There was a problem hiding this comment.
4. Silent peers leave sockets open forever 🐞 Bug ☼ Reliability
writer_task loops back into WriterPoll after transmitting a Close frame, and that future remains pending until peer_close or shared.closed is set because no close-handshake deadline exists. When an upstream accepts the frame but never answers with Close, both the transport half and the service-worker streams entry remain retained indefinitely.
Agent Prompt
## Issue description
A locally initiated WebSocket close waits forever when the peer never returns a Close frame, retaining the transport task and service-worker stream registration.
## Issue Context
The writer must allow the peer a reasonable close-handshake interval, but must terminate the transport and surface an abnormal close when that interval expires. Ensure normal peer-close echoes still preserve their status and reason.
## Fix Focus Areas
- crates/zp-kernel-bundle/src/kernel/transport/ws_client.rs[704-775]
- crates/zp-kernel-bundle/src/kernel/transport/ws_client.rs[816-854]
- web/sw.js[2737-2757]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| result.__zpFetchMeta = { | ||
| url: u, redirected: opt.redirectDepth > 0, | ||
| type: opt.mode === 'no-cors' && new URL(u).origin !== context.origin ? 'opaque' : 'basic', | ||
| }; |
There was a problem hiding this comment.
5. Cross-origin fetches report wrong type 🐞 Bug ≡ Correctness
transportFetchHop sets response metadata to basic for every non-opaque result, without selecting cors for a successful cross-origin CORS-mode fetch. When a page fetches another virtual origin with mode: "cors", createFetchResponseAdapter exposes that metadata through Response.type, so page code observes basic instead of the browser's cors result.
Agent Prompt
## Issue description
Successful cross-origin CORS Fetch responses are labeled `basic`, causing the page-visible `Response.type` to differ from native Fetch behavior.
## Issue Context
Choose response metadata from the request mode and the final response origin. Preserve `opaque` for cross-origin `no-cors`, use `cors` for successful cross-origin CORS requests, and keep `basic` for same-origin results.
## Fix Focus Areas
- web/sw.js[1927-1932]
- web/zp-core.js[287-327]
- test/js/request-policy.test.js[129-158]
- test/e2e/request-contract.js[39-56]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| let streamable = matches!(coding.as_str(), "" | "identity" | "gzip" | "x-gzip" | "deflate"); | ||
| if null_body || executable || !streamable { |
There was a problem hiding this comment.
6. Compressed responses can exhaust memory 🐞 Bug ☼ Reliability
finish_response selects the streaming path for gzip and deflate bodies, where decode_readable_stream forwards every decoded chunk without tracking cumulative decoded bytes. A target can send a highly expanding compressed non-executable response and a consumer that keeps reading will receive unlimited decoded data, unlike the buffered path that enforces MAX_DECODED_BYTES.
Agent Prompt
## Issue description
The streaming gzip/deflate path does not enforce the decoded response size limit, so compressed responses can continuously expand while downstream consumers drain them.
## Issue Context
Buffered response handling rejects bodies beyond `MAX_DECODED_BYTES`, but the streamable coding branch bypasses that check and delegates decompression to `DecompressionStream`.
## Fix Focus Areas
- crates/zp-kernel-bundle/src/kernel/transport/http2.rs[327-379]
- crates/zp-kernel-bundle/src/kernel/transport/http2.rs[436-463]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Qodo Fixer🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (3) 🔗 Fix PR: #3 This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR. Prompt for coding agent Process — 3 fixed
|
|
| const result = addCSP(resp, opt.request, opt.tab.servers, opt.tab, u); | ||
| result.__zpFetchMeta = { | ||
| url: u, redirected: opt.redirectDepth > 0, | ||
| type: opt.mode === 'no-cors' && new URL(u).origin !== context.origin ? 'opaque' : 'basic', |
There was a problem hiding this comment.
A cross-origin runtime fetch() using mode: "cors" and credentials: "include" sends cookies for the destination origin, but the final response is marked basic without checking the target's Access-Control-Allow-Origin or Access-Control-Allow-Credentials headers. This lets a proxied page read credentialed responses that a native browser would reject.
How this was verified: The credential gate permits cookies for include, while the response path returns a readable basic response without validating the target's CORS headers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/sw.js
Line: 1930
Comment:
**Enforce Target CORS**
A cross-origin runtime `fetch()` using `mode: "cors"` and `credentials: "include"` sends cookies for the destination origin, but the final response is marked `basic` without checking the target's `Access-Control-Allow-Origin` or `Access-Control-Allow-Credentials` headers. This lets a proxied page read credentialed responses that a native browser would reject.
**How this was verified:** The credential gate permits cookies for `include`, while the response path returns a readable `basic` response without validating the target's CORS headers.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
27 issues found across 180 files
Not reviewed (too large): .ai/trap-notebook/rewriter.md (~3,071 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".ai/trap-notebook/README.md">
<violation number="1" location=".ai/trap-notebook/README.md:19">
P2: 현재 INDEX의 `LOG.md#2026-08-24-1` 같은 링크는 실제 앵커로 이동하지 않습니다. LOG에 명시적 앵커를 추가하거나 링크를 실제 heading slug로 갱신하고, `static-policy`도 선언된 앵커를 파싱하도록 고쳐야 이 새 계약이 유효합니다.</violation>
</file>
<file name=".ai/trap-notebook/real-site-compat.md">
<violation number="1" location=".ai/trap-notebook/real-site-compat.md:5">
P2: These new historical links use fragments that do not match the headings in `LOG.md`, so both links land nowhere. Use `#2026-08-18-15-membrane` and `#2026-08-18-18-real-site-compat`, or add explicit anchors.</violation>
<violation number="2" location=".ai/trap-notebook/real-site-compat.md:17">
P2: The 6 June cross-references use fragments that do not exist in this rewritten file, so readers cannot follow the cited SDK mitigation entry. Replace both fragments with the exact explicit anchor ID on the following entry.</violation>
<violation number="3" location=".ai/trap-notebook/real-site-compat.md:32">
P2: These references still point to the removed `membrane.md`, so the rewritten notes expose three dead links. Retarget them to existing `rewriter.md`/`LOG.md` anchors or remove the obsolete references.</violation>
</file>
<file name="scripts/test.mjs">
<violation number="1" location="scripts/test.mjs:19">
P2: When Chromium or the relay hits a transient `ECONNRESET`, `runE2E` now fails immediately because the new runner has no retry path. Restore a bounded retry for E2E transport resets so intermittent relay disconnects do not fail CI.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:14">
P3: WebTransport and WebRTC are not separate Go services in this checkout: `main.go` runs both as optional components of the same process. Describe WebTransport as using the separately configured `-wt-addr` listener and WebRTC as running on the main HTTP listener so operators do not look for nonexistent service binaries.</violation>
<violation number="2" location="README.md:97">
P2: This Tor configuration does not provide stream isolation: the kernel sends `Auth::None` for every SOCKS5 CONNECT, so `IsolateSOCKSAuth` cannot separate streams by tab or user. Document the current non-isolated behavior, or wire per-tab username/password credentials before advertising this setup as isolated.</violation>
</file>
<file name=".ai/trap-notebook/transport-regression.md">
<violation number="1" location=".ai/trap-notebook/transport-regression.md:17">
P3: The new `LOG.md` fragments do not match the headings they reference, so these history links do not navigate to their entries. Use each heading’s full generated slug or add explicit anchors.</violation>
</file>
<file name=".ai/trap-notebook/sw-integration.md">
<violation number="1" location=".ai/trap-notebook/sw-integration.md:30">
P2: The source-evidence links in this summary are broken from `.ai/trap-notebook/`, so readers cannot open the implementation references. Use `../../web/...` for root-level web files and update or remove the obsolete relay and kernel paths.</violation>
</file>
<file name=".ai/trap-notebook/wasm-page-rt.md">
<violation number="1" location=".ai/trap-notebook/wasm-page-rt.md:5">
P3: The new background references point to `.ai/zp-page-rt-design.md` and `.ai/zp-page-rt-bench-report.md`, neither of which exists in this checkout. Restore them or update every reference to preserved documents so readers do not hit dead links.</violation>
<violation number="2" location=".ai/trap-notebook/wasm-page-rt.md:74">
P3: The whitelist link targets `main.go#L120`, but current line 120 is RTC/TURN setup; the `/__zp/zp_page_rt.wasm` route is around lines 243-247. Change the anchor to the route.</violation>
<violation number="3" location=".ai/trap-notebook/wasm-page-rt.md:98">
P3: The cited `runtime-prelude.js#L2415-L2435` range is synchronous XHR code, not the MO cache cleanup. Link the current callback range around lines 6712-6731 so the historical claim remains verifiable.</violation>
</file>
<file name="crates/zp-kernel-bundle/src/kernel/transport/ws_client.rs">
<violation number="1" location="crates/zp-kernel-bundle/src/kernel/transport/ws_client.rs:771">
P2: When the peer never sends a Close response, the Rust writer waits forever after sending the local Close and the reader stays blocked on the socket. The page’s 30-second guard only closes the JavaScript object, so the SW/kernel stream and relay connection leak; add an independent close-handshake timeout that aborts both drivers and surfaces `1006` if no peer Close arrives.</violation>
</file>
<file name="crates/zp-kernel-bundle/src/kernel/transport/fetch.rs">
<violation number="1" location="crates/zp-kernel-bundle/src/kernel/transport/fetch.rs:647">
P2: When an upstream response contains either internal stream-marker name, `Headers.append` produces a comma-joined value and the SW no longer recognizes the marker as exactly `1`. The response then bypasses `completeBodyResponse`, so the live body can outlive the SW's completion tracking and the internal marker can reach the page; delete these names before adding the proxy-owned markers.</violation>
</file>
<file name=".ai/trap-notebook/INDEX.md">
<violation number="1" location=".ai/trap-notebook/INDEX.md:10">
P3: 이 요약은 handshake UA의 출처를 잘못 묶습니다. 상세 기록처럼 UA는 고정 browser persona, Origin은 검증된 요청 문서, Cookie는 목적지 jar로 구분해 적어야 합니다.</violation>
<violation number="2" location=".ai/trap-notebook/INDEX.md:39">
P3: 이 행은 서로 다른 두 기록을 합쳤지만 `#2026-08-24-4`는 O(N²) 성능만 설명합니다. 요약을 연결된 절의 성능 내용으로 좁히거나 컬렉션 표면 기록을 별도 항목으로 연결해야 합니다.</violation>
<violation number="3" location=".ai/trap-notebook/INDEX.md:41">
P2: 이 요약은 아직 측정하지 않은 채널 격리를 완료된 보장처럼 기록합니다. sessionStorage와 진단 키만 확인된 사실로 적고 BroadcastChannel·SharedWorker는 미측정이라고 명시해야 합니다.</violation>
<violation number="4" location=".ai/trap-notebook/INDEX.md:67">
P3: 이 행의 링크는 detached view만 설명하고 scratchpad 덮어쓰기 순서는 설명하지 않습니다. 요약을 `#2026-05-30-22`의 memory.grow 계약으로 좁히거나 scratch 기록을 별도 항목으로 추가해야 합니다.</violation>
</file>
<file name=".ai/trap-notebook/build-deploy.md">
<violation number="1" location=".ai/trap-notebook/build-deploy.md:64">
P2: When the toolchain is missing, following this recovery entry does not install `wasm-bindgen-cli`, so the subsequent build still cannot run. Restore the `cargo install` command in this note.</violation>
</file>
<file name=".ai/trap-notebook/tls-fingerprint.md">
<violation number="1" location=".ai/trap-notebook/tls-fingerprint.md:5">
P3: The new 7/28 correction link does not target the LOG heading, so readers cannot jump to the cited retraction. Link to `LOG.md#2026-07-28-2-transport-regression` or add an explicit anchor.</violation>
<violation number="2" location=".ai/trap-notebook/tls-fingerprint.md:22">
P3: The compressed notebook leaves readers with dead or misleading implementation citations. Update the moved `crates/zp-bundle` paths to `crates/zp-kernel-bundle` and refresh the source line anchors for the current files.</violation>
</file>
<file name="test/js/rewriter.test.js">
<violation number="1" location="test/js/rewriter.test.js:113">
P3: The second assertion in this test checks `ctx.window.result === '/next'`, but that value is leftover from the first `window.location = "/next"` rewrite, not from the extensionless-target rewrite under test. If the second rewrite regressed, the assertion can still pass against the stale value, so the extensionless-path case is not actually verified. Give it a fresh `location` (or an explicit expected value) so its output reflects the target URL it rewrote.</violation>
</file>
<file name="crates/zp-kernel-bundle/src/kernel/transport/decode.rs">
<violation number="1" location="crates/zp-kernel-bundle/src/kernel/transport/decode.rs:24">
P2: For any unsupported or non-decodable Content-Encoding, decode_body now returns Err and http2.rs's `decode_body(...)?` aborts the whole response, whereas the previous version passed the encoded body through and kept the residual encoding header. A server using an unrecognized codec (or a raw-DEFLATE body labeled `deflate`, which ZlibDecoder rejects) will now fail to load entirely. Consider returning the body with a residual encoding list (or a sentinel) instead of Err so the response still renders, matching the earlier pass-through behavior.</violation>
</file>
<file name="test/e2e/proxy.test.js">
<violation number="1" location="test/e2e/proxy.test.js:1247">
P3: Multiple subtests inside the same parent share identical names ('clientCookie' twice, 'upstream request identity' seven times across two groups). node:test disambiguates them only by index, so `--test-name-pattern`, CI failure titles, and TAP output cannot tell which check failed. Give each subtest a distinct name describing what it asserts.</violation>
</file>
<file name=".github/workflows/ci.yml">
<violation number="1" location=".github/workflows/ci.yml:188">
P2: The `test:wasm:ci` step is placed after browser-install but has no `if` condition, so it inherits the default `success()` gating. When only the Chrome install fails, the browser-independent WASM tests are skipped along with it, losing the WASM 12/12 coverage. Decouple it with `if: ${{ !cancelled() }}` and keep the e2e step gated on `steps.browser-install.outcome == 'success'`.</violation>
</file>
<file name="crates/zp-rewriter/src/lib.rs">
<violation number="1" location="crates/zp-rewriter/src/lib.rs:557">
P3: Every dangerous-member compound write, update, destructuring target, and for-in/of target now allocates a fresh `{b, get v, set v}` object literal per evaluation to preserve a settable Reference. In hot loops (e.g. `location.hash++`, or `for (target.prop of ...)` iterating many elements) this is a per-iteration allocation where native code was allocation-free, and it diverges from the plain-write `__zp_set` fast path this same change documents as 'allocation-free'. The wrapper can't simply be cached because the receiver differs per evaluation, but for the common loop case a loop-invariant target could be materialized once. Worth a note before merge even if intentionally accepted.</violation>
</file>
<file name="crates/zp-kernel-bundle/src/kernel/transport/http2.rs">
<violation number="1" location="crates/zp-kernel-bundle/src/kernel/transport/http2.rs:271">
P2: The h2 reader now finishes only on END_STREAM; the old Content-Length / decode-end short-circuit that closed the body as soon as the declared bytes arrived was removed. For upstreams that deliver the full body but withhold the h2 END_STREAM frame (the NAVER case the deleted code explicitly worked around), buffered subresources and stream completion will block until the peer's idle timeout instead of finishing at Content-Length. Restore a content-length-based completion check for the buffered path so a complete body can finish without waiting on END_STREAM.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| ## INDEX 계약 | ||
| `| YYYY-MM-DD | 분류 / 상태 | 한 문장 요약 | category.md#실재앵커 |` | ||
| - 한 항목은 한 줄, 요약은 160자 이하. 측정 덤프와 조사 과정은 상세에만 둔다. | ||
| - 상세 제목에 `{#앵커}` 또는 `<a id="앵커"></a>`를 두고 실제 존재하는 앵커를 연결한다. |
There was a problem hiding this comment.
P2: 현재 INDEX의 LOG.md#2026-08-24-1 같은 링크는 실제 앵커로 이동하지 않습니다. LOG에 명시적 앵커를 추가하거나 링크를 실제 heading slug로 갱신하고, static-policy도 선언된 앵커를 파싱하도록 고쳐야 이 새 계약이 유효합니다.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/README.md, line 19:
<comment>현재 INDEX의 `LOG.md#2026-08-24-1` 같은 링크는 실제 앵커로 이동하지 않습니다. LOG에 명시적 앵커를 추가하거나 링크를 실제 heading slug로 갱신하고, `static-policy`도 선언된 앵커를 파싱하도록 고쳐야 이 새 계약이 유효합니다.</comment>
<file context>
@@ -1,85 +1,30 @@
+## INDEX 계약
+`| YYYY-MM-DD | 분류 / 상태 | 한 문장 요약 | category.md#실재앵커 |`
+- 한 항목은 한 줄, 요약은 160자 이하. 측정 덤프와 조사 과정은 상세에만 둔다.
+- 상세 제목에 `{#앵커}` 또는 `<a id="앵커"></a>`를 두고 실제 존재하는 앵커를 연결한다.
+- 같은 조사는 한 줄로 합친다. 후속 정정이 앞 가설을 대체함을 상세에 명시한다.
+- `test/js/static-policy.test.js`가 인덱스 길이·링크 계약을 검사한다.
</file context>
| - (b) **URL 을 쿼리스트링에 실어 나르는 설계는 GET 폼 제출에서 반드시 파괴된다** (쿼리 통째 교체). 런처류 URL 을 form action 에 쓰려면 타깃은 **path segment** 에 있어야 안전하다. | ||
| - (c) 증상이 한 사이트의 한 기능(NAVER 검색)으로 보여도, 원인이 prelude 공통 경로면 **전 사이트 전 폼**이 깨진 상태다. 사이트별 회귀로 분류하기 전에 공통 경로인지 확인할 것. | ||
| - **원인**: webpack Automatic publicPath가 `/zp/api/fetch?url=...`의 쿼리와 마지막 segment를 제거하여 `/zp/api/<chunk>`로 요청, ChunkLoadError를 냈다. `crates/zp-htmltx/src/lib.rs`의 `absolute_target_url`/`data-zp-target-url` 추가와 script/link getter의 원본 URL 반환으로 대응했다. | ||
| - **최종 정정**: “transform/element handler가 실행되지 않아 stash가 없다”는 가설은 틀렸다. SW `transformHtml` sentinel로 속성 주입을 확인했고, DOM에서 안 보인 것은 [의도된 마스킹](membrane.md#2026-05-30--zp-속성-마스킹-회로)이었다. getter의 `urlMeta → Native.getAttribute → native getter` 경로로 publicPath가 정상 계산됐다. |
There was a problem hiding this comment.
P2: These references still point to the removed membrane.md, so the rewritten notes expose three dead links. Retarget them to existing rewriter.md/LOG.md anchors or remove the obsolete references.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/real-site-compat.md, line 32:
<comment>These references still point to the removed `membrane.md`, so the rewritten notes expose three dead links. Retarget them to existing `rewriter.md`/`LOG.md` anchors or remove the obsolete references.</comment>
<file context>
@@ -1,691 +1,117 @@
-- (b) **URL 을 쿼리스트링에 실어 나르는 설계는 GET 폼 제출에서 반드시 파괴된다** (쿼리 통째 교체). 런처류 URL 을 form action 에 쓰려면 타깃은 **path segment** 에 있어야 안전하다.
-- (c) 증상이 한 사이트의 한 기능(NAVER 검색)으로 보여도, 원인이 prelude 공통 경로면 **전 사이트 전 폼**이 깨진 상태다. 사이트별 회귀로 분류하기 전에 공통 경로인지 확인할 것.
+- **원인**: webpack Automatic publicPath가 `/zp/api/fetch?url=...`의 쿼리와 마지막 segment를 제거하여 `/zp/api/<chunk>`로 요청, ChunkLoadError를 냈다. `crates/zp-htmltx/src/lib.rs`의 `absolute_target_url`/`data-zp-target-url` 추가와 script/link getter의 원본 URL 반환으로 대응했다.
+- **최종 정정**: “transform/element handler가 실행되지 않아 stash가 없다”는 가설은 틀렸다. SW `transformHtml` sentinel로 속성 주입을 확인했고, DOM에서 안 보인 것은 [의도된 마스킹](membrane.md#2026-05-30--zp-속성-마스킹-회로)이었다. getter의 `urlMeta → Native.getAttribute → native getter` 경로로 publicPath가 정상 계산됐다.
+- **검증·미해결**: `external_stylesheet_link_rewritten` 단위 테스트 통과, 실제 다수 chunk 로드·rspack queue 활성화·React `loaded` 도달. 그러나 GitHub ErrorPage는 남았다. 데이터 fetch/라우터/SSR-CSR mismatch는 추정일 뿐이며 타 사이트 회귀는 당시 미관찰. path 기반 URL 전환은 과거 제안이다.
</file context>
| ## 2026-06-05 — NAVER 지문 노출·루프 캡·taskweaver 환경 | ||
|
|
||
| **Why it hid so long**: `form.action` **프로퍼티** 는 `installURLProp` 이 가상화해서 정상적으로 실제 타깃(`https://search.naver.com/search.naver`)을 반환한다. 그래서 콘솔에서 `f.action` 을 찍어보면 멀쩡해 보인다. 깨진 건 `getAttribute('action')` 쪽 뿐이고, 하필 제출 코드가 그걸 썼다. 또 `data-zp-target-url` 은 `installStealthMembrane` 이 마스킹하므로 `getAttribute` 로는 `null` 로 보여 "htmltx 가 stash 를 안 했나" 로 오독하기 쉽다 — 실제로는 stash 되어 있고 `urlMeta` 에 살아 있다. | ||
| - **원인**: [6월 4일 기록](#2026-06-04) 이후에도 warm-session wedge 지속. 멤브레인 함수의 native `toString` 마스크는 대체로 정상이었지만, non-enumerable `window.ZP`/`ZeroProxyRT`는 `getOwnPropertyNames(window)`에 노출됐다. taskweaver는 Chrome이 아니라 WebView2+Tauri이며 `__TASKWEAVER_*`, `__TAURI*`, `ipc` 등 통제 밖 host globals도 주입한다. |
There was a problem hiding this comment.
P2: The 6 June cross-references use fragments that do not exist in this rewritten file, so readers cannot follow the cited SDK mitigation entry. Replace both fragments with the exact explicit anchor ID on the following entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/real-site-compat.md, line 17:
<comment>The 6 June cross-references use fragments that do not exist in this rewritten file, so readers cannot follow the cited SDK mitigation entry. Replace both fragments with the exact explicit anchor ID on the following entry.</comment>
<file context>
@@ -1,691 +1,117 @@
+## 2026-06-05 — NAVER 지문 노출·루프 캡·taskweaver 환경
-**Why it hid so long**: `form.action` **프로퍼티** 는 `installURLProp` 이 가상화해서 정상적으로 실제 타깃(`https://search.naver.com/search.naver`)을 반환한다. 그래서 콘솔에서 `f.action` 을 찍어보면 멀쩡해 보인다. 깨진 건 `getAttribute('action')` 쪽 뿐이고, 하필 제출 코드가 그걸 썼다. 또 `data-zp-target-url` 은 `installStealthMembrane` 이 마스킹하므로 `getAttribute` 로는 `null` 로 보여 "htmltx 가 stash 를 안 했나" 로 오독하기 쉽다 — 실제로는 stash 되어 있고 `urlMeta` 에 살아 있다.
+- **원인**: [6월 4일 기록](#2026-06-04) 이후에도 warm-session wedge 지속. 멤브레인 함수의 native `toString` 마스크는 대체로 정상이었지만, non-enumerable `window.ZP`/`ZeroProxyRT`는 `getOwnPropertyNames(window)`에 노출됐다. taskweaver는 Chrome이 아니라 WebView2+Tauri이며 `__TASKWEAVER_*`, `__TAURI*`, `ipc` 등 통제 밖 host globals도 주입한다.
+- **수정·규칙**: [web/zp-core.js](../../web/zp-core.js), [web/zp-rt.js](../../web/zp-rt.js)를 configurable로 바꾸고 [prelude IIFE](../../web/runtime-prelude.js)에서 클로저 캡처 후 삭제했다. Symbol 설치 마커는 유지하되 `getOwnPropertySymbols`에는 보인다. [rewriter](../../crates/zp-rewriter/src/lib.rs)는 상수 무한 `for/while/do`에 캡을 추가하고 유한 조건은 유지했다. body의 break/continue/closure 의미를 보존하며 중첩 카운터를 분리했다.
+- **검증·한계**: 루프 단위 테스트 7개 통과. 사용자 수동 msedge 검증에서는 광고·뉴스·쇼핑·날씨 포함 풀 렌더, wedge 미관찰; taskweaver에서는 여전히 wedge. 당시에는 host 지문이 효과를 가리는 주원인으로 판단했다. Chrome 동일성은 추정, Firefox는 미검증이며 NAVER에서 루프 캡 발동도 미관찰이다. SDK sandbox·chained reflection 조정·recursive timeout 캡은 당시 제안뿐이다. [전날 SDK mitigation](#2026-06-04-—-naver-warm-session-v8-wedge) 참조.
</file context>
| 실사이트에서 관찰한 과거 원인·수정·반증 기록이며 **현재 승인 사양이 아니다**. 측정·검증은 모두 당시 결과이고, 이번 문서 편집에서 런타임 실행이나 새 측정은 없었다. 과거 제안 테스트·구현 공백은 미검증 이력이지 현재 TODO가 아니다. 제거된 ARCHITECTURE/PHASE 상태 문서는 역사적 참고이며 현재 권위가 아니다. 후속 정정이 앞선 가설보다 우선한다. | ||
|
|
||
| --- | ||
| 현재 상태: GitHub 에러 페이지와 CF 전체 콜드 호환성은 미해결로 유지한다. CNN 두 번째 정지는 원인 미확정·미재현이며, 뒤의 대기시간/광고 구성 정정과 개별 회복 관찰도 전체 완료가 아니다. CF의 과거 [eval 수정](LOG.md#2026-08-18-15)과 [clearance 착시](LOG.md#2026-08-18-18)는 구별한다. |
There was a problem hiding this comment.
P2: These new historical links use fragments that do not match the headings in LOG.md, so both links land nowhere. Use #2026-08-18-15-membrane and #2026-08-18-18-real-site-compat, or add explicit anchors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/real-site-compat.md, line 5:
<comment>These new historical links use fragments that do not match the headings in `LOG.md`, so both links land nowhere. Use `#2026-08-18-15-membrane` and `#2026-08-18-18-real-site-compat`, or add explicit anchors.</comment>
<file context>
@@ -1,691 +1,117 @@
+실사이트에서 관찰한 과거 원인·수정·반증 기록이며 **현재 승인 사양이 아니다**. 측정·검증은 모두 당시 결과이고, 이번 문서 편집에서 런타임 실행이나 새 측정은 없었다. 과거 제안 테스트·구현 공백은 미검증 이력이지 현재 TODO가 아니다. 제거된 ARCHITECTURE/PHASE 상태 문서는 역사적 참고이며 현재 권위가 아니다. 후속 정정이 앞선 가설보다 우선한다.
----
+현재 상태: GitHub 에러 페이지와 CF 전체 콜드 호환성은 미해결로 유지한다. CNN 두 번째 정지는 원인 미확정·미재현이며, 뒤의 대기시간/광고 구성 정정과 개별 회복 관찰도 전체 완료가 아니다. CF의 과거 [eval 수정](LOG.md#2026-08-18-15)과 [clearance 착시](LOG.md#2026-08-18-18)는 구별한다.
-## 2026-07-30 — 모든 GET 폼 제출이 프록시 자신을 타깃으로 삼음 (NAVER 검색 "찾을 수 없음")
</file context>
| 현재 상태: GitHub 에러 페이지와 CF 전체 콜드 호환성은 미해결로 유지한다. CNN 두 번째 정지는 원인 미확정·미재현이며, 뒤의 대기시간/광고 구성 정정과 개별 회복 관찰도 전체 완료가 아니다. CF의 과거 [eval 수정](LOG.md#2026-08-18-15)과 [clearance 착시](LOG.md#2026-08-18-18)는 구별한다. | |
| 현재 상태: GitHub 에러 페이지와 CF 전체 콜드 호환성은 미해결로 유지한다. CNN 두 번째 정지는 원인 미확정·미재현이며, 뒤의 대기시간/광고 구성 정정과 개별 회복 관찰도 전체 완료가 아니다. CF의 과거 [eval 수정](LOG.md#2026-08-18-15-membrane)과 [clearance 착시](LOG.md#2026-08-18-18-real-site-compat)는 구별한다. |
| encoding: 'utf8', | ||
| maxBuffer: 64 * 1024 * 1024, | ||
| function run(args, extraEnv = {}) { | ||
| const result = spawnSync(process.execPath, args, { |
There was a problem hiding this comment.
P2: When Chromium or the relay hits a transient ECONNRESET, runE2E now fails immediately because the new runner has no retry path. Restore a bounded retry for E2E transport resets so intermittent relay disconnects do not fail CI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/test.mjs, line 19:
<comment>When Chromium or the relay hits a transient `ECONNRESET`, `runE2E` now fails immediately because the new runner has no retry path. Restore a bounded retry for E2E transport resets so intermittent relay disconnects do not fail CI.</comment>
<file context>
@@ -1,53 +1,52 @@
- encoding: 'utf8',
- maxBuffer: 64 * 1024 * 1024,
+function run(args, extraEnv = {}) {
+ const result = spawnSync(process.execPath, args, {
+ cwd: repoRoot,
+ env: { ...env, ...extraEnv },
</file context>
| (→ Wikipedia/ja4-기반 사이트는 통과) 이지만 **ja3_hash, peetprint_hash 불일치**. 필드별: | ||
| - **원인:** Phase 5.7의 captured spec은 extension 순서에만 적용되고 cipher는 `config.provider.cipher_suites`를 그대로 사용했다. Chrome 134 시기 grouped cipher, SCSV·자동 padding, Firefox 계열 `record_size_limit`, UA/TLS/sec-ch-ua 버전 불일치가 남아 있었다. SW `Headers.entries()`의 정렬로 wire 헤더가 알파벳순이었고, Accept-CH grant 없이 opt-in hints를 전달했으며 kernel이 UA를 끝에 append했다. | ||
| - **TLS 수정:** [third_party-rustls-fork/src/client/hs.rs:392-410](../../third_party-rustls-fork/src/client/hs.rs)에서 captured cipher를 wire에 적용하되 provider 목록은 ServerHello 선택 검증용으로 유지했다. captured 사용 시 SCSV를 생략하고 padding은 captured에 있을 때만 추가했다. legacy RSA cipher는 당시 decoy로 광고했으며, 관측한 모던 서버는 구현된 TLS 1.3/ECDHE 계열을 선택했다. | ||
| - **baseline 수정:** [web/sw.js:495](../../web/sw.js#L495)의 captured spec을 Chrome 148 cipher·extension 순서로 교체(ECH 제외, captured curves에는 MLKEM 포함). [tls.rs:91-119](../../crates/zp-bundle/src/kernel/transport/tls.rs#L91-L119)의 fallback cipher도 AES128/AES256/CHACHA별 ECDSA→RSA interleaved 순서로 정렬했다. **captured curves에 존재한다는 사실은 실제 지원·wire 적용을 뜻하지 않는다.** 아래 MLKEM revert가 최종 정정이다. |
There was a problem hiding this comment.
P3: The compressed notebook leaves readers with dead or misleading implementation citations. Update the moved crates/zp-bundle paths to crates/zp-kernel-bundle and refresh the source line anchors for the current files.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/tls-fingerprint.md, line 22:
<comment>The compressed notebook leaves readers with dead or misleading implementation citations. Update the moved `crates/zp-bundle` paths to `crates/zp-kernel-bundle` and refresh the source line anchors for the current files.</comment>
<file context>
@@ -1,395 +1,70 @@
-(→ Wikipedia/ja4-기반 사이트는 통과) 이지만 **ja3_hash, peetprint_hash 불일치**. 필드별:
+- **원인:** Phase 5.7의 captured spec은 extension 순서에만 적용되고 cipher는 `config.provider.cipher_suites`를 그대로 사용했다. Chrome 134 시기 grouped cipher, SCSV·자동 padding, Firefox 계열 `record_size_limit`, UA/TLS/sec-ch-ua 버전 불일치가 남아 있었다. SW `Headers.entries()`의 정렬로 wire 헤더가 알파벳순이었고, Accept-CH grant 없이 opt-in hints를 전달했으며 kernel이 UA를 끝에 append했다.
+- **TLS 수정:** [third_party-rustls-fork/src/client/hs.rs:392-410](../../third_party-rustls-fork/src/client/hs.rs)에서 captured cipher를 wire에 적용하되 provider 목록은 ServerHello 선택 검증용으로 유지했다. captured 사용 시 SCSV를 생략하고 padding은 captured에 있을 때만 추가했다. legacy RSA cipher는 당시 decoy로 광고했으며, 관측한 모던 서버는 구현된 TLS 1.3/ECDHE 계열을 선택했다.
+- **baseline 수정:** [web/sw.js:495](../../web/sw.js#L495)의 captured spec을 Chrome 148 cipher·extension 순서로 교체(ECH 제외, captured curves에는 MLKEM 포함). [tls.rs:91-119](../../crates/zp-bundle/src/kernel/transport/tls.rs#L91-L119)의 fallback cipher도 AES128/AES256/CHACHA별 ECDSA→RSA interleaved 순서로 정렬했다. **captured curves에 존재한다는 사실은 실제 지원·wire 적용을 뜻하지 않는다.** 아래 MLKEM revert가 최종 정정이다.
+- **HTTP 수정:** [web/sw.js:636-708](../../web/sw.js)에서 grant 없는 device-memory/downlink/dpr/ect/rtt, 상세 sec-ch-ua, viewport/save-data/prefers 계열 hints를 drop하고 Chrome 헤더 순서를 강제했다. UA를 inline 배치하고 [kernel/mod.rs:258](../../crates/zp-bundle/src/kernel/mod.rs#L258)의 `user-agent` strip을 제거했다. [web/zp-core.js:22](../../web/zp-core.js#L22)의 fallback UA를 148로 갱신하되 page native UA가 우선한다.
+- **당시 검증·한계:** tls.peet.ws에서 cipher tuple은 Chrome 148과 byte-equivalent였지만 전체 JA3/JA4는 달랐고 ECH·group/key_share 차이가 남았다. `nid.naver.com`은 `TARGET_CONNECT_FAILED`에서 로그인 페이지 로드로 회복했으나 HTTP slow-lane은 잔존했다. “wire 작업 완료, IP만 문제” 결론은 실 wire 검증 없이 내려진 오판이었다. 직접 Chrome의 지연만으로 IP 원인을 확정한 이 항목의 주장도 아래 session 비교가 정정한다.
</file context>
|
|
||
| 상위 plan: PHASE2 strict mode "탈출 없는 감옥". client-TLS 가 서버로 누출되지 않는 | ||
| 것이 첫 번째 invariant — 모든 fingerprint mimicry 는 그 invariant 안에서. | ||
| 보안 불변식: **client-TLS가 서버로 누출되어서는 안 되며, fingerprint mimicry도 이 경계를 우회할 수 없다.** 아래 초기의 “wire 완성·IP 원인” 결론은 후속 wire 검사와 session 비교로 정정되었다. NAVER의 서로 다른 지연·렌더 장애를 하나의 원인으로 합치지 않는다. 특히 [7/28 정정](LOG.md#2026-07-28-2)은 END_STREAM/deflate withholding 서사를 yamux lost wakeup으로 철회했다. 아래 7/3 GREASE/binder 결함은 이 전체 지연의 최종 원인 선언이 아니다. |
There was a problem hiding this comment.
P3: The new 7/28 correction link does not target the LOG heading, so readers cannot jump to the cited retraction. Link to LOG.md#2026-07-28-2-transport-regression or add an explicit anchor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .ai/trap-notebook/tls-fingerprint.md, line 5:
<comment>The new 7/28 correction link does not target the LOG heading, so readers cannot jump to the cited retraction. Link to `LOG.md#2026-07-28-2-transport-regression` or add an explicit anchor.</comment>
<file context>
@@ -1,395 +1,70 @@
-상위 plan: PHASE2 strict mode "탈출 없는 감옥". client-TLS 가 서버로 누출되지 않는
-것이 첫 번째 invariant — 모든 fingerprint mimicry 는 그 invariant 안에서.
+보안 불변식: **client-TLS가 서버로 누출되어서는 안 되며, fingerprint mimicry도 이 경계를 우회할 수 없다.** 아래 초기의 “wire 완성·IP 원인” 결론은 후속 wire 검사와 session 비교로 정정되었다. NAVER의 서로 다른 지연·렌더 장애를 하나의 원인으로 합치지 않는다. 특히 [7/28 정정](LOG.md#2026-07-28-2)은 END_STREAM/deflate withholding 서사를 yamux lost wakeup으로 철회했다. 아래 7/3 GREASE/binder 결함은 이 전체 지연의 최종 원인 선언이 아니다.
----
</file context>
| const { ctx, location } = executionContext(); | ||
| vm.runInContext(rewrite('window.location = "/next";', 'classic', 'https://target.example/app.js?ts=12#frag'), ctx); | ||
| assert.equal(location.href, '/next'); | ||
| vm.runInContext(rewrite('window.result = location.href;', 'classic', 'https://target.example/challenge/v1?ray=abc'), ctx); |
There was a problem hiding this comment.
P3: The second assertion in this test checks ctx.window.result === '/next', but that value is leftover from the first window.location = "/next" rewrite, not from the extensionless-target rewrite under test. If the second rewrite regressed, the assertion can still pass against the stale value, so the extensionless-path case is not actually verified. Give it a fresh location (or an explicit expected value) so its output reflects the target URL it rewrote.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/js/rewriter.test.js, line 113:
<comment>The second assertion in this test checks `ctx.window.result === '/next'`, but that value is leftover from the first `window.location = "/next"` rewrite, not from the extensionless-target rewrite under test. If the second rewrite regressed, the assertion can still pass against the stale value, so the extensionless-path case is not actually verified. Give it a fresh `location` (or an explicit expected value) so its output reflects the target URL it rewrote.</comment>
<file context>
@@ -1,262 +1,273 @@
+ const { ctx, location } = executionContext();
+ vm.runInContext(rewrite('window.location = "/next";', 'classic', 'https://target.example/app.js?ts=12#frag'), ctx);
+ assert.equal(location.href, '/next');
+ vm.runInContext(rewrite('window.result = location.href;', 'classic', 'https://target.example/challenge/v1?ray=abc'), ctx);
+ assert.equal(ctx.window.result, '/next');
});
</file context>
| await t.test('setCookieBody', () => { assert.equal(runtimeIntegration.setCookieBody, 'set-cookie-ok'); }); | ||
| await t.test('serverCookie', () => { assert.match(runtimeIntegration.serverCookie, /target_server=from-target/); }); | ||
| await t.test('visibleCookie', () => { assert.match(runtimeIntegration.visibleCookie, /client_runtime=from-runtime/); }); | ||
| await t.test('clientCookie', () => { assert.match(runtimeIntegration.clientCookie, /target_server=from-target/); }); |
There was a problem hiding this comment.
P3: Multiple subtests inside the same parent share identical names ('clientCookie' twice, 'upstream request identity' seven times across two groups). node:test disambiguates them only by index, so --test-name-pattern, CI failure titles, and TAP output cannot tell which check failed. Give each subtest a distinct name describing what it asserts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/proxy.test.js, line 1247:
<comment>Multiple subtests inside the same parent share identical names ('clientCookie' twice, 'upstream request identity' seven times across two groups). node:test disambiguates them only by index, so `--test-name-pattern`, CI failure titles, and TAP output cannot tell which check failed. Give each subtest a distinct name describing what it asserts.</comment>
<file context>
@@ -949,47 +1210,133 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
+ await t.test('setCookieBody', () => { assert.equal(runtimeIntegration.setCookieBody, 'set-cookie-ok'); });
+ await t.test('serverCookie', () => { assert.match(runtimeIntegration.serverCookie, /target_server=from-target/); });
+ await t.test('visibleCookie', () => { assert.match(runtimeIntegration.visibleCookie, /client_runtime=from-runtime/); });
+ await t.test('clientCookie', () => { assert.match(runtimeIntegration.clientCookie, /target_server=from-target/); });
+ await t.test('clientCookie', () => { assert.match(runtimeIntegration.clientCookie, /client_runtime=from-runtime/); });
+ await t.test('stream.status', () => { assert.equal(runtimeIntegration.stream.status, 200); });
</file context>
| await t.test('clientCookie', () => { assert.match(runtimeIntegration.clientCookie, /target_server=from-target/); }); | |
| await t.test('clientCookie server cookie', () => { assert.match(runtimeIntegration.clientCookie, /target_server=from-target/); }); |
| // A leading '(' would join a preceding ASI statement. | ||
| out.push_str("0,"); | ||
| } | ||
| out.push_str(&format!( |
There was a problem hiding this comment.
P3: Every dangerous-member compound write, update, destructuring target, and for-in/of target now allocates a fresh {b, get v, set v} object literal per evaluation to preserve a settable Reference. In hot loops (e.g. location.hash++, or for (target.prop of ...) iterating many elements) this is a per-iteration allocation where native code was allocation-free, and it diverges from the plain-write __zp_set fast path this same change documents as 'allocation-free'. The wrapper can't simply be cached because the receiver differs per evaluation, but for the common loop case a loop-invariant target could be materialized once. Worth a note before merge even if intentionally accepted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/zp-rewriter/src/lib.rs, line 557:
<comment>Every dangerous-member compound write, update, destructuring target, and for-in/of target now allocates a fresh `{b, get v, set v}` object literal per evaluation to preserve a settable Reference. In hot loops (e.g. `location.hash++`, or `for (target.prop of ...)` iterating many elements) this is a per-iteration allocation where native code was allocation-free, and it diverges from the plain-write `__zp_set` fast path this same change documents as 'allocation-free'. The wrapper can't simply be cached because the receiver differs per evaluation, but for the common loop case a loop-invariant target could be materialized once. Worth a note before merge even if intentionally accepted.</comment>
<file context>
@@ -532,6 +536,32 @@ pub fn apply_patches(source: &str, patches: &[Patch]) -> String {
+ // A leading '(' would join a preceding ASI statement.
+ out.push_str("0,");
+ }
+ out.push_str(&format!(
+ "({{b:({}),get v(){{return __zp_get(this.b,{:?})}},set v(v){{__zp_set(this.b,{:?},v)}}}}).v",
+ obj_src, prop, prop
</file context>
변경 사항
검증
a484e7b의 CI 전체 통과c9e6e62는 문서·주석만 정리한 커밋이며 중복 CI를 생략함범위
기존 CI/E1 회귀를 해결한 변경입니다. 전체 리팩터링 로드맵이나 GitHub/Cloudflare/CNN 등 모든 실사이트의 호환성 완료를 의미하지 않습니다. 후속 범위는
.ai/design/website-compat-refactor.md에 정리했습니다.Summary by cubic
Restores browser compatibility and re-enables CI/E2E coverage by fixing request/response handling, streaming, WebSocket, and DOM/srcdoc regressions so the full test suite passes.
What changed
Validation
Written for commit c9e6e62. Summary will update on new commits.