* [runway] RUNWAY_PORT_BASE moves the harness ports
The scramjet harness, its wisp server and the bare harness were fixed at
4500-4502, so two runway instances (or anything else on those ports)
could not run side by side. RUNWAY_PORT_BASE moves all three, and the
harness page finds wisp next to its own port.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MJ6gCzt6WZNrhpy8GvnNb
* [core] URL fragments: keep them verbatim and keep fragment navigations in the document
The fragment went through the URL codec, so the real URL's fragment was
not the site's: `#a/b` became `#a%2Fb`, which broke `:target`, native
scroll-to-anchor, text fragments and `location.hash` round trips. It is
never sent anywhere, so it now stays as written. The JS rewriter
encoded it into static import URLs too, so `import "./m.js#a"` and
`import("./m.js#a")` could load two instances.
In-page `#x` navigations reloaded the document: rewritten URLs carry
the navigator's query params (`$io`, ...) and the document's real URL
carries its loader's, so the browser never saw the same document. A
navigation to the document's own URL (links, location setters, assign,
replace, `window.open(url, "_self")`, meta refresh, controller.go) now
goes to its real URL with the new fragment, and the browser applies its
own fragment-navigation and replace rules. Fragment-only hyperlinks are
left relative, so they resolve when followed - after pushState too.
That relies on the real <base href> being a proxy URL, which it was
not: anything left relative resolved against the real site. It is now
rewritten (ignored `data:`/`javascript:` bases are left for the browser
to ignore). The client's base URL resolved a relative base against the
origin rather than the document URL; only the first base element with
an href counts; about:blank and srcdoc documents fall back to their
creator's base URL.
History: pushState/replaceState implement "can have its URL
rewritten" rather than comparing origins, resolve against the base URL,
treat "" like null as Chrome does, and keep the document's own request
params on the real entry URL, so a frame reloads as a frame.
Also: CSS `url(#id)` is left alone (gradients, clip paths, masks,
filters), blob URLs keep their fragment (`#t=5`, `#page=2`), window
handler content attributes (onhashchange, onpopstate, onmessage, ...)
are rewritten instead of running against the real globals, and
`_top`/`_parent` target keywords are matched case-insensitively.
Tests: runway fragments.ts (end to end against bare Chrome, reached
cross-origin the way real pages are) and rewriter-fragments.ts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MJ6gCzt6WZNrhpy8GvnNb
* [core] fragment review fixes: escaping, base elements, self-navigation, file: history
- A fragment put back into a static import's string literal is escaped
for one. The fragment percent-encode set leaves `\` and `'`, so
`import "./m.js#a\\"` came out as invalid JavaScript. The module's own
URL had the same problem in `$scramjet$meta(import.meta, "...")` and
`$scramjet$import("...", ...)`: the service worker sees the fragment a
module was requested with, and the rewriter put it in unescaped. Both
now go through one escape function, and Flags carries the escaped base.
- Only an HTML base element in the document's own tree sets the base URL.
The service worker adopted the first `<base>` it saw, including one in
a <template> or an SVG/MathML element named base, so later scripts
loaded from the wrong directory. The client matched `base[href]`, which
also finds the SVG one; it now checks the namespace.
- A navigation to the document's own URL only reuses its real URL when it
has a fragment - when it stays in the document. Without one it loads a
new document, and reusing the real URL kept the initiator of whoever
first loaded it (`$io`), so `location.assign(location.href)` after
entering from another origin was sent as Sec-Fetch-Site: same-site. The
cost is that such a navigation pushes a history entry where the browser
replaces one (fragment-self-navigation-replaces-entry, listed failing).
A <base> naming the document still resolves to its real URL, since it
loads nothing itself.
- "can have its URL rewritten" lets a file: URL change its query, as HTML
says; it fell through to the fragment-only rule. Moved out of the
history interceptor so it can be tested on its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MJ6gCzt6WZNrhpy8GvnNb
* [core] the HTML parser records each element's namespace
Whether a <base> sets the document's base URL depends on it being an
HTML element, and the rewriter guessed that from ancestor names. The
parser's own foreign-content tracking made the same guess: `title`,
`mi` and `annotation-xml` counted as integration points in any
namespace, `annotation-xml`'s `encoding` was never read, and breakout
start tags (`<p>`, `<div>`, `<font color>` ... inside <svg>) did not
end foreign content. So `<math><annotation-xml><base>` set the base
URL, and so did a base in an HTML-looking spot that Chrome puts in SVG;
an SVG element named `template` was taken for an inert HTML one.
The parser now decides each element's namespace by the tree
construction rules - MathML text and HTML integration points,
`annotation-xml` by its encoding, breakout tags including `font` with
color/face/size - and DomBuilder records it on the Element. The
tokenizer's raw-text, CDATA and self-closing decisions read the same
state, and only an HTML element is void (an SVG `base` has children).
The base check is now `namespace === "html"` outside an HTML template.
Parser tests pin namespaces to the trees Chrome builds for the same
markup; runway covers the base cases both ways.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MJ6gCzt6WZNrhpy8GvnNb
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
A client's id is how a posted message names its sender: the recipient looks
it up in the box's clientIds to answer MessageEvent.source. That only works
if no two clients in the box share one, and a random draw cannot promise it.
Math.random and crypto.getRandomValues keep their state per realm, and an
engine can be made to start every realm from the same seed (V8's
--random-seed); then every realm that installs its own client draws the same
first value, clientIds keeps whichever registered last, and every message
reads as sent by that one.
Turnstile hit exactly this: its widget, a later srcdoc frame and the page
all drew one id, so the widget's messages came back sourced from the srcdoc
frame, failed the widget's e.source === iframe.contentWindow check, and the
challenge's completion was never accepted.
The box is the only place an id is resolved, so the box numbers them. That
is unique by construction, and takes nothing out of the page's own random
sequence.
The test pins Math.random per realm before any script runs and checks that
two frames and the page are each named as the sender of their own message.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Brings the safe-iteration work over from the now-outdated
feat/safe-iteration (9d627e36, c0332df0) and applies it to everything that
has landed since. That branch's other commit, 91264d0a, is already on
develop through the stack.
`for...of`, spread and array destructuring all run the iteration protocol,
whose lookups land on `Array.prototype[@@iterator]` and
`%IteratorPrototype%` - both writable by the page. Any of them in the page
realm lets a page change what scramjet believes it read, or what it
passes on after checking: `el.prepend(...args)` spread `args` into the
native after `text.around` had inspected them, so a replaced array
iterator could hand the native a different node.
- `drain(xs)` in the snapshot: a for...of-able view of an array built
from own properties, so none of the protocol's lookups reach the page.
- `no-unsafe-iteration` lint rule, at "error" for src/client and
src/Tap.ts, with tests. Its messages now point at `drain` and
`Reflect_apply`.
- All 45 sites it found: spreads into natives become
`Reflect_apply(super.m, this, args)` (node.ts, reflect.ts, css.ts,
settimeout, importScripts); loops become `drain`; the NamedNodeMap
fallback indexes the native rather than running its iterator; Headers
are copied with the native's own `forEach`. The brand-check plugin
learns that `Reflect_apply(super.m, this, ...)` calls the native.
`observedAttributes` keeps `for...of`: it is the page's own iterable,
and the browser runs its protocol too.
- Things the rule cannot see: `Promise_all` still walked its argument
with the protocol and called each element's `then`; it is now a
snapshot helper over `Promise_then`, and the unused `Promise_race`,
`Promise_allSettled` and `Promise_any` are gone. Tap.dispatch, which
runs in the page on every navigation, drops spread, `.map`, `.find`,
`.includes` and `.push`. `Array.prototype` method calls in
client.ts, chrome.ts, event.ts, err.ts and webidl.ts go through the
snapshot, and `client.native`'s getters and setters, and
sourcemaps' toString, stop going through `Function.prototype.call`.
- Cold indexed loops that only read `xs[i]` read as
`for (const x of drain(xs))` now. Hot paths stay indexed: the text
layer, per-write attribute rules, instanceof, per-call IDL
conversion, stack formatting and performance entry lists. So do live
platform collections, which `drain` cannot be handed.
- The commented-out `window.postMessage` proxy from the wrapPostMessage
era is deleted.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AWUAE5wuAo6WKM7y5TncPC
Supersedes #115, redone against develop now that the stack has landed.
From #115:
- `scramjet-core/intercept-brand-check` goes "warn" -> "error".
- Dead bindings: `Function_call` in client.ts, plus unused imports in
entry.ts, err.ts, sourcemaps.ts and shared/index.ts.
- The type-generation rspack target gets `performance: { hints: false }`
like the other four.
Getting brand-check to zero took a plugin fix, not suppressions. It
missed two ways of reaching a native: lib.dom declares `style` (and every
other attribute whose setter takes a different type) as a `get`/`set`
pair, which `nativeMember` did not recognise; and `super.x = v` runs the
native setter on `this`, which the plugin treated as never checking.
It now also reads lib.webworker for SharedWorkerGlobalScope. That clears
11 false positives. The rest:
- `Document.execCommand` really could return without touching the
receiver. It now reads the receiver's own selection via
`super.getSelection()` rather than the window's, which is also the
correct document for a cross-frame call.
- `CSSMarginRule.style`, `CSSPositionTryRule.style` and
`Element.setHTML` do reach `super` on every path, but lib.dom does not
have them yet, so they are disabled with that reason.
Bugs found along the way:
- Worker `postMessage` called `super.postMessage` inside a GlobalScope
static. As `GlobalScope`'s own doc says, `super` there only sees the
global's own properties, and a worker keeps its members on
`WorkerGlobalScope.prototype`, so it was undefined. It now goes
through `client.native.window(this)`, as the window variant does too.
- The postMessage IDL `(sequence<object> or optional
StructuredSerializeOptions = {})` is not valid IDL and failed the type
level parser; it is now `optional (sequence<object> or
StructuredSerializeOptions) = {}`.
tsc: 11 errors -> 0. Besides postmessage: `moveBefore` moves to the
ParentNode mixin where it belongs, `ScramjetRequestMode` (never existed)
becomes lib.dom's `RequestMode`, wrap.ts gets honest Window types, and
data: URL blobs get a `Uint8Array<ArrayBuffer>`.
eslint: 53 errors / 30 warnings -> 0 / 5. The no-globals errors now go
through snapshot wrappers (`_URL`, `_Uint8Array`, `_Map`, ...), with
`_Blob`, `WebAssembly_Module` and `selfLocation` added. sourcemaps.ts
reads its u32s off the bytes rather than a page-replaceable DataView.
entry.ts's load-time environment probes are exempt, with the reason
given. `quotes` gets `avoidEscape` so it stops fighting prettier.
Tool tests: the intercept fixture's fake box was missing
`objectPrototypes`, which `relevantPromise` now reads, and the "refuses
declarations without a checkReceiver" test is flipped to match
e438c4db, which removed that refusal on purpose.
Claude-Session: https://claude.ai/code/session_01AWUAE5wuAo6WKM7y5TncPC
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* [core] remove wrapPostMessage and add the incumbency test matrix
Carved out of the IDL interceptor branch: the postMessage-adjacent
changes, kept together because the rewriter half and the client half
have to move at the same time.
- The `$scramjet$wrappostmessage` rewriter function is gone, along with
the `wrappostmessagefn` config key and the rust that emitted it
(`visitor.rs`, `rewrite.rs`, `changes.rs`, `cfg.rs`). Wrapping every
`postMessage` callee at rewrite time was both a large hit on rewritten
output and unnecessary now that the client intercepts the member.
- `runway/src/incumbence.ts` is new: a matrix harness for incumbent
settings object behaviour, plus the `incumbent-*` tests that drive it.
- `adversarial/postmessage-targetorigin.ts` covers the targetOrigin
check, which is the one postMessage argument that is an origin
comparison.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* trace the incumbent settings object
Resolves a collision with the element rewrite that neither branch could
see on its own: both lineages independently grew a `base64ToBytes` and a
`base64ToBytesFallback` in `shared/util.ts`. Merged naively the bodies
do not conflict - only the import list does - so the file ends up with
two definitions of each and does not compile.
Kept the implementations from this branch. The fallback here fills a
`new _Uint8Array(len)` by index, where the element one used
`_Uint8Array.from(binString, ...)`, and `from` over a string runs the
page-replaceable iteration protocol - the thing the no-unsafe-iteration
rule exists to catch.
Kept `base64Decode` from the element side, which is new there and has no
counterpart here; it reads through the surviving `base64ToBytes`.
`Array_from` and `String_fromCodePoint` leave the imports with the code
that used them.
* fully working pst based incumbent settings object
* stamp/lazystamp part 1
* stamp js side
* [runway] allowlist the incumbency matrix's known failures
`runway` exits 1 on any failure that is not in `failing_tests.json`
(`unexpectedFailing`), and only *reports* allowlisted tests that have
started passing (`noLongerFailing`). So the two directions are not
symmetric: a PR that fixes tests can leave the file alone and stay
green, but a PR that adds known-failing tests cannot.
The previous commit adds the incumbency matrix, 44 of whose tests fail
today - that is the point of the matrix, it is the gap being measured.
Without these entries this branch reports 52 unexpected failures and
exits 1.
Also drops the 13 entries the series fixes, which is correct at this
point in the stack: they are the cachestorage and interceptor tests the
per-API rewrites repair, and everything that repairs them is already
merged below this commit.
This is the one piece of `failing_tests.json` that could not wait for
the cleanup PR. The seven per-API PRs still leave the file untouched,
because each of them only moves tests from failing to passing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* remove a comment
* kill nonce again???
* incumbency and postmessage fixes 1
* add backup incumbency through bind proxy
* [core] read the incumbency mode for the top-level frame
The incumbency mode is a flag like any other, and a subframe has to run
with its top-level frame's: the client reads it for client.topUrl and
the rewriter for flagsUrl(meta), so the two can no longer pick
different modes for the same script.
Replaces the script-URL override test, which asserted the old
behaviour, with one checking that a subframe inherits the top-level
frame's mode over the one its own URL matches.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NY4qYbtVkrhABqAUszUNt6
* incumbency fixes
* incumbency fixes?
* whatever
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Flags were matched against whichever URL happened to be at hand - a
subframe's own, a script's, a worker's - so one page could run with
several sets of flags at once, and the service worker's rewriter could
disagree with the client about them.
URLMeta now carries topUrl, the top-level frame's URL, and every flag
is read for it. The client finds it by walking scramjet-controlled
parents (parentFrame/topmostClient, which topFrameName and
parentFrameName now share); the service worker from a new $top
parameter where it cannot tell, otherwise from the requesting client.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NY4qYbtVkrhABqAUszUNt6
* [core] intercept setAttributeNode so detached Attr writes are rewritten
Carved out of the IDL interceptor branch. `Element.prototype.
setAttributeNode` was intercepted with an empty body, so
const attr = document.createAttribute("src");
attr.value = "https://origin.example/x.js";
script.setAttributeNode(attr);
reached no rewriter at all: `dom/attr.ts` only routes `Attr.value` back
through `setAttribute` for an Attr that already has an `ownerElement`,
and a freshly created one does not. Whether the resulting load stayed
inside the proxy came down to whether the service worker happened to be
controlling the document.
Rewritten in place on the node rather than replayed through
`setAttribute`, so that `el.getAttributeNode(name) === attr` and
`attr.ownerElement` both keep holding.
Covered by `adversarial/attribute-node-bypass.ts`.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* rewrite element 2
Full rewrite of the element/attribute layer onto the IDL interceptors.
Scramjet rewrites content attributes: `src` on an image is a proxy URL
in the live DOM, `nonce` is gone entirely, `style` has been through the
CSS rewriter. None of that may be visible to the page, so every
rewritten attribute is now *mirrored* - the value the page wrote is kept
alongside it in `scramjet-attr-<name>`, and every read answers out of
the mirror.
The rules are the same ones the HTML rewriter applies while parsing, so
an attribute written by script ends up in exactly the state it would
have been in had it come down in the markup. Everything that touches an
attribute goes through one `attributeAccess` path: this module's
`Element` members, `dom/attr.ts` for `Attr` and `NamedNodeMap`,
`dom/reflect.ts` for the reflected IDL attributes. Nothing reads
`scramjet-attr-` by hand any more.
New files: `dom/attr.ts`, `dom/node.ts`, `dom/reflect.ts`,
`dom/markup.ts`, `dom/frames.ts`. `CharacterData` moves to `dom/node.ts`,
keeping the arity declarations - without them `node.appendData()`
rewrote and appended the string "undefined" where the native owes the
page a TypeError.
Supersedes the `setAttributeNode` interception from the previous commit
with one that also covers `setAttributeNodeNS`.
Drops the stale `SetHTMLUnsafeOptions: object` placeholder from
`webidl.ts`: this branch models the dictionary properly in
`global.d.ts`, so keeping both declared it twice.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* [core] fix the element layer's regressions and remaining escapes
Review fixes for the element/attribute rewrite, each covered by
runway/src/tests/adversarial/element-layer.ts (24 of its 28 tests fail
without this commit).
Regressions against develop:
- script.textContent / innerText ran unrewritten: Blink defines both again
on HTMLScriptElement.prototype (Trusted Types sinks), shadowing the Node
and HTMLElement interceptors. Intercepted there too.
- A relative <base href> broke every URL getter: the native base still
started with the prefix and unrewrote to a relative string. Resolved from
the base element and the fallback base URL instead.
- setAttributeNode with an Attr the element already owns rewrote the
rewritten value into the mirror. An owned Attr now goes straight to the
native (no-op or InUseAttributeError).
- Appending to a parsed script rewrote it twice and duplicated the text:
existing Text children are now recorded before any mutation.
- removeAttributeNS / removeNamedItemNS left a stripped attribute's mirror.
Escapes:
- setHTMLUnsafe, setHTML (style), insertAdjacentHTML and outerHTML into a
script or style, Range.insertNode, Range.surroundContents and moveBefore
all landed raw text in a script.
- SVG script href (and xlink:href under any prefix) was never rewritten.
- box.instanceof honoured a page-defined Symbol.hasInstance, and decided
both text blanking and the markup rewriter's foreign context. Now
OrdinaryHasInstance, with native brand/namespace checks at those sites.
- meta refresh content set by script was never rewritten; it is now a rule
in htmlRules, re-run when http-equiv changes.
Correctness:
- Text blanked for an insertion is restored if the native throws.
- Text inserted into a connected, not-yet-started script now runs: Blink
does not prepare a script on a data write, so the script is re-prepared
with a native insert/remove once the rewritten program is in place.
- normalize() on an ancestor keeps its scripts' text; the source is
recorded before the write that can run the script.
- nonce is the [[CryptographicNonce]] slot, including on MathML.
- The style mirror follows CSSOM and typed OM writes; iframe.sandbox is a
real DOMTokenList over a detached stand-in; NamedNodeMap methods keep
their identity; insertAdjacentText throws SyntaxError on a bad position.
- textContent no longer walks subtrees with no script or style in them.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fp1ZD3JFmSXkxjonJv6BMV
* [core] reach natives through client.native in the element layer
The element layer captured descriptors out of `client.nativeStore` and
called them with `Reflect_apply`, in all six of its modules (attr,
element, fragments, markup, node, reflect) - the only DOM code in the
tree that does. Everything goes through `new client.native.X(obj)` now,
the same as the rest of client/, and the comment in element.ts arguing
for the snapshotted descriptors is gone.
Also stops `getSVGDocument` answering through `this.contentDocument`,
which the page can shadow, in favour of the getter's own steps.
No behaviour change: runway (no WPT) is identical before and after.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fp1ZD3JFmSXkxjonJv6BMV
* [core] make the attribute and text layers classes owned by the client
Both layers were module-level `WeakMap`s keyed on the client, filled
lazily by `attributeAccess(client)` / `textAccess(client)` with a record
of closures over that client - global state with a lifetime nothing
tied to the client, reachable by anything that imported the module.
They are now `AttributeLayer` (client/attributes.ts) and `TextLayer`
(client/text.ts), constructed in the `ScramjetClient` constructor as
`client.attributes` and `client.text`. They live outside client/dom/
because every module there is loaded as an interceptor.
The pure name helpers (`isInternalAttribute`, `mirrorAttributeName`, ...)
move with the attribute layer and stay free functions, holding no state.
`insertAttributeNode` becomes `AttributeLayer.insertNode`, and attr.ts's
`attrValue` / `setAttrValue` become `visibleValue` / `setVisibleValue`,
so nothing reaches into another interceptor module for shared logic.
No behaviour change: runway (no WPT) matches the previous run, bar the
live-site site-youtube-search test, which passes on its own.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fp1ZD3JFmSXkxjonJv6BMV
* elementlayer tests
* fix failing html tests
* vendor htmlparser2
* another review round
* more review + insert proxy finally
* more review 2
* some bullshit
* naiive attribute selector rewriter
* small perf improvements
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [core] rewrite WebSocket and add WebSocketStream onto client.Intercept
Moves the socket APIs off the hand-rolled traps and onto the IDL
interceptors from the base PR.
- `requests/websocket.ts` -> `requests/WebSocket.ts`, rewritten as an
intercepted class.
- `requests/WebSocketStream.ts` is new: the API had no interceptor at
all, so a page could reach the real origin through it.
- Restores `socketmap`/`socketstreammap` on the singleton box. These
are typed on `FakeWebSocketState`/`FakeWebSocketStreamState`, which
arrive with this PR, so the base could not carry them.
A fake socket is an `EventTarget`, so its events go out through
`dispatchEvent` and carry `isTrusted: false`. The box's `trustedEvents`
set (base PR) is what makes them read as trusted; `adversarial/
websocket-fidelity.ts` covers that along with the rest of the surface.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* websocket proper dispatch
* fix?
* small prototype fix
* small fuxes
* more tests
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [core] rewrite the event interceptor onto client.Intercept
Moves `shared/event.ts` off the hand-rolled traps and onto the IDL
interceptors from the base PR. Behaviour changes are in how events are
identified and how synthetic ones are marked, not in the set of events
handled.
- `addEventListener`/`removeEventListener` take their options through
`readAddEventListenerOptions`/`readEventListenerOptions` rather than
reading properties off the argument directly, so a page cannot
observe or intercept the read.
- Listener bookkeeping moves from `client.eventcallbacks` to the
singleton box's `eventhandlers`, keyed per realm.
- `isTrusted` for events scramjet synthesizes is answered from the
box's `trustedEvents` set rather than by defining the property on the
instance. `isTrusted` is a prototype accessor, so an own property
there is itself the tell.
- `MessageEvent.origin` now reads the `$scramjet$messagetype` stamp
that `postmessage.ts` already writes, instead of inferring the shape
from whether an origin happens to be present. Messages that are not
ours - service worker and embedder control messages - answer with the
site's origin rather than the proxy's.
- `alwaysLastBubble` (utils and inject) takes `stopPropagation` and
`stopImmediatePropagation` from `nativeStore` once at setup instead
of constructing a native wrapper per event.
`adversarial/events-fidelity.ts` covers the above against the bare
harness as oracle.
Verified against feat/idl-groundwork: rspack builds all bundles; tsc,
the types build and eslint are all unchanged from the base (26 errors /
11 files, 7, and 283 problems respectively). All are pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* fixes
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Moves the CSS, document and related DOM surface off the hand-rolled
traps and onto the IDL interceptors from the base PR.
- `dom/css.ts` is rewritten: URL rewriting now goes through the
CSSOM interfaces rather than restringifying rules, so a page reading
back `cssText` sees what it wrote.
- `dom/Navigator.ts` and `dom/Audio.ts` are new interceptors for
surfaces that previously had none.
- `dom/document.ts`, `dom/fragments.ts` (Range), `dom/fontface.ts` and
`shared/blob.ts` move onto Intercept with IDL-typed members.
Deliberately excludes `dom/element.ts` and `dom/attr.ts`: those belong
to the element rewrite and land there.
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [core] rewrite fetch, XHR, EventSource and headers onto client.Intercept
Moves the request APIs off the hand-rolled traps and onto the IDL
interceptors from the base PR.
- `requests/xmlhttprequest.ts` -> `requests/XMLHttpRequest.ts` and
`requests/eventsource.ts` -> `requests/EventSource.ts`, both rewritten
as intercepted classes rather than trap tables.
- `shared/headers.ts` grows the carried-header vocabulary:
`CARRIED_HEADER_PREFIX`, `carriedHeaderName`, `isCarriedHeaderName`
and `uncarriedHeaderName`. Headers scramjet has to move across the
proxy boundary are now named by one rule rather than ad hoc per site.
- `dom/beacon.ts` is gone; `sendBeacon` is handled with the rest of the
request surface.
Tests: `request-response-fidelity`, `headers-view` and `xhr-fidelity`,
all differential against the bare harness.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* fixes
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [core] rewrite the storage and cache interceptors onto client.Intercept
Moves CacheStorage, IndexedDB and Web Storage off the hand-rolled traps
and onto the IDL interceptors from the base PR. All three are origin
scoping fixes to interceptors that already existed.
- `shared/caches.ts` keys cache entries by the site's URL rather than
the rewritten one, so two origins proxied through scramjet no longer
share a cache entry.
- `dom/storage.ts` scopes `localStorage`/`sessionStorage` per site
origin instead of per proxy origin.
- `shared/indexeddb.ts` moves onto Intercept with IDL-typed members.
Tests: `cache-keying`, `cache-storage` and `storage-scope-leaks`, the
last covering the cross-origin reads the old keying allowed.
The Cookie Store API was split out of this PR: it is a new interceptor
for a surface that had none, not a scoping fix, and it shares nothing
with these three.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* fixes
* [runway] update failing_tests for the storage and cache fixes
Eight entries pass now and come out: `storage-clear`,
`storage-delete-property`, `storage-key-by-index` and
`storage-missing-property-is-undefined` from the Web Storage rewrite,
`cachestorage-add-and-addall`, `cachestorage-keys-not-namespaced` and
`cachestorage-request-keys-urls` from the cache one, and
`platform-indexeddb-databases` from scoping `databases()`.
`cachekey-retrieved-response-is-fully-corrected` goes in. The test is
cross-PR: this branch ships the `tag()` that marks a cache entry as
having come off the network, and `shared/requests/fetch.ts` on
feat/idl-requests is what reads `box.taggedResponses` to unrewrite the
URL and put the stripped headers back. It passes once both land, and
the entry comes out then.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018Zn7Pst62DZXqzGyrkQ68o
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
`Intercept` threw for any declaration that converts IDL arguments and
does not pass a `checkReceiver`. No call site has ever passed one - 19
declarations on develop, zero with a receiver check - so the refusal
fired for every such interface, and `loadModules` swallowed the throw
into a `dbg.error`. Because it aborts the module function, everything
declared after the offending member went with it.
10 of the 19 declarations on develop were being refused: CookieStore,
Document.cookie, History, all five of performance.ts, StorageManager
and FileSystemHandle behind it, and Worklet. The runway failures are a
readout of exactly that set - ckjar-*, cookies-*, the pushstate tests,
platform-performance-observer, platform-resource-timing-own-entries -
none of which fail on 060552a4, before #110 landed.
The ordering it guarded is real: Web IDL brand-checks the receiver
before converting arguments, and conversion runs page code. But the
predicate asked whether a member declares IDL arguments at all, not
whether its conversion can run any page code, so `@Arguments()` with no
types was refused too. And a promise-only interface like Cache or
CookieStore has no member that can satisfy the contract at all, since
they reject rather than throw. Refusing to install is much the worse
outcome: for the storage-shaped modules it means no origin scoping,
which is a cross-origin leak traded for a fingerprint.
#117 tracks the real fix - a predicate that asks whether a conversion
can run page code, an async brand check, and a build-time failure.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018Zn7Pst62DZXqzGyrkQ68o
Moves the last of the per-API surface off the hand-rolled traps and
onto the IDL interceptors from the base PR.
- `dom/open.ts` implements `window.open` through the base PR's
`openWindowSteps`, and answers `frameElement` about the window the
page asked about rather than the proxy's.
- `dom/history.ts` keeps `pushState`/`replaceState` URLs in the site's
origin.
- `dom/performance.ts` unrewrites the URLs in performance entries, so
timing data stops naming the proxy.
- `shared/settimeout.ts` moves onto Intercept with IDL-typed members.
Tests: `frame-element` (the one Window attribute whose whole job is an
origin comparison - under scramjet every proxied document genuinely is
same-origin, so that step has to be re-made against the origins the
sites themselves have) and `origin-scoping`.
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* [core] rewrite the worker, OPFS and BroadcastChannel interceptors
Moves the worker surface off the hand-rolled traps and onto the IDL
interceptors from the base PR.
- `shared/broadcastchannel.ts` is new: the channel name was not scoped
per origin, so two sites proxied through scramjet could see each
other's messages.
- `shared/opfs.ts` scopes the origin-private file system per site
origin rather than per proxy origin.
- `worker/importScripts.ts` and `shared/worker.ts` resolve script URLs
against the site's origin.
Tests: `opfs` and `broadcastchannel-scope`, the latter covering the
cross-origin delivery the unscoped name allowed.
* review and remove comments
---------
* [core] add client.Intercept and the WebIDL type machinery
Groundwork for replacing the RawProxy/RawTrap interceptors with
declarative, IDL-typed ones. No API is migrated here - this lands the
machinery and the differential tests that cover it, so the per-API
rewrites can land in parallel on top.
- `client/webidl.ts`: the IDL type language. Parses signature strings
into types at compile time, and compiles them into argument
validators and brand checkers at runtime. `Arguments`, `Returns`,
`Type` and `Constructor` annotate an interceptor member; unresolved
or malformed type names surface as a TS error on the annotation.
- `client.Intercept`: takes a class whose members carry those
annotations, splices it onto the native prototype chain, and wires
each member through a trampoline so `error.stack` and
`Function.prototype.toString` keep reporting the native shape.
- `client/nativeerror.ts` + `shared/error.ts`: identify frames from the
client bundle by a frame taken inside it rather than by file name, so
stack cleaning holds however the embedder serves the bundle. This is
what makes `cleanErrors` safe to turn on by default - with it off,
every intercepted member leaves `scramjet.js` frames in any stack a
page reads.
- `shared/snapshot.ts`: more pinned natives, all additive.
- `client/shared/origin.ts` + `client/helpers.ts`: the origin model the
per-API rewrites build on. Replaces `dom/origin.ts` and
`dom/protocol.ts`.
- `tools/eslint/brand-check-plugin.mjs`: flags interceptor members
whose IDL annotation and body disagree about brand checking. Wired in
at "warn" - it will go to "error" once the APIs are migrated.
- `tools/prettier/intercept-hug.mjs`: keeps the class argument of
`client.Intercept(...)` hugged, so the migrations don't each
reformat their own file.
- `allowFailedIntercepts` is gone: an interceptor that fails now throws
rather than silently calling through with unvalidated input.
Three adversarial tests cover the machinery rather than any one API -
`interceptor-nativeness` (the shape of the patches), `webidl-this`
(brand checks and `this` handling) and `interface-object-identity`.
Verified against origin/develop: rspack builds all bundles; tsc goes
43 -> 32 errors over the same file set; the types build goes 7 -> 4
erroring files; eslint goes 276 -> 261 problems. All remaining failures
are pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* clean up comments
* small fixes
* nits
* preverse __proto__
* [core] intercept the Cookie Store API (#114)
* [core] intercept the Cookie Store API
Split out of the storage PR: this is a new interceptor for an API that
had none, rather than a scoping fix to an existing one.
`CookieStore` read and wrote the proxy's real cookie jar directly, so a
site could see and set cookies belonging to the proxy origin and to
every other site proxied through it.
`shared/cookie.ts` grows `getCookieList`, which returns the matching
records rather than a serialized header. The matching rules are the same
ones `getCookies` already applied for `document.cookie`; the Cookie
Store API needs the attributes as well as the values, so the two now
share one implementation instead of parsing the serialized form back
apart.
`dom/cookie.ts` moves onto the shared matching and onto Intercept.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* clean comments slightly
* [core] address review on the Cookie Store interceptor
`serialize` concatenates `path` into Set-Cookie text but only checked that
it starts with '/'. The spec does not reject a ';' there, so the native
would have accepted one and set it structurally; we build a header, and
`path: "/x;Domain=victim.com;Path=/"` read back out of the jar's parser as
a cookie scoped to another site's host — the same cross-site write this
interceptor exists to close, reached through the one attribute that was
not checked. Rejected now, along with control characters, and thrown
rather than delegated: handing it to the native would write for real.
The domain-match check compared against `client.scopeUrl.hostname` while
`commit` keys the jar write on `client.url`. Those are two different
hosts, and `scopeUrl`'s own docblock says never to compare against it —
an opaque document's is a random `about-opaque://` stand-in. Compare
against the host the write actually lands on.
`get()` and `get({})` are a TypeError per step 5 of the get(options)
steps; they were answering with the first cookie in the jar. `getAll()`
has no such step and keeps returning everything.
Also: `import type` for `Cookie`, no stray const splitting the import
block, no reach through the page's `Array.prototype.indexOf` for the
SameSite enum, `_`-prefixed unused params so eslint is clean, the
brand-check note once above the class rather than deleted entirely, a
note that `partitioned` isolates nothing without a partition key, and
dom/cookie.ts drops the `_Date`/`_URL` import it never used.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01QxzkcFw9NRS2XLpgxZwdh9
* [core] drop the brand-check comment again
Restoring it in the previous commit undid acfc2def on purpose-removed
comments; it was not wanted.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01QxzkcFw9NRS2XLpgxZwdh9
* [core] drop nativeInit, take WriteInit from the reader
`nativeInit` copied an object we had already built ourselves, field by
field, out of values the readers had already converted — `super.set(init)`
forwards nothing of the page's. And `WriteInit` restated by hand exactly
the shape `readCookieInit` returns, so the CookieInit member list lived in
three places and two of them would go stale the next time the IDL grows a
member.
The half of nativeInit's docblock that is still load-bearing — why a
delegated call cannot have the native write as a side effect — moves onto
`serialize`, next to the return value it is about.
---------
* [core] add client.Intercept and the WebIDL type machinery
Groundwork for replacing the RawProxy/RawTrap interceptors with
declarative, IDL-typed ones. No API is migrated here - this lands the
machinery and the differential tests that cover it, so the per-API
rewrites can land in parallel on top.
- `client/webidl.ts`: the IDL type language. Parses signature strings
into types at compile time, and compiles them into argument
validators and brand checkers at runtime. `Arguments`, `Returns`,
`Type` and `Constructor` annotate an interceptor member; unresolved
or malformed type names surface as a TS error on the annotation.
- `client.Intercept`: takes a class whose members carry those
annotations, splices it onto the native prototype chain, and wires
each member through a trampoline so `error.stack` and
`Function.prototype.toString` keep reporting the native shape.
- `client/nativeerror.ts` + `shared/error.ts`: identify frames from the
client bundle by a frame taken inside it rather than by file name, so
stack cleaning holds however the embedder serves the bundle. This is
what makes `cleanErrors` safe to turn on by default - with it off,
every intercepted member leaves `scramjet.js` frames in any stack a
page reads.
- `shared/snapshot.ts`: more pinned natives, all additive.
- `client/shared/origin.ts` + `client/helpers.ts`: the origin model the
per-API rewrites build on. Replaces `dom/origin.ts` and
`dom/protocol.ts`.
- `tools/eslint/brand-check-plugin.mjs`: flags interceptor members
whose IDL annotation and body disagree about brand checking. Wired in
at "warn" - it will go to "error" once the APIs are migrated.
- `tools/prettier/intercept-hug.mjs`: keeps the class argument of
`client.Intercept(...)` hugged, so the migrations don't each
reformat their own file.
- `allowFailedIntercepts` is gone: an interceptor that fails now throws
rather than silently calling through with unvalidated input.
Three adversarial tests cover the machinery rather than any one API -
`interceptor-nativeness` (the shape of the patches), `webidl-this`
(brand checks and `this` handling) and `interface-object-identity`.
Verified against origin/develop: rspack builds all bundles; tsc goes
43 -> 32 errors over the same file set; the types build goes 7 -> 4
erroring files; eslint goes 276 -> 261 problems. All remaining failures
are pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01U6ry3tx3Z9nWXwLTJBRDMk
* clean up comments
* small fixes
* nits
* preverse __proto__
---------
Share address parsing and configured search providers between the address bar and new-tab page. Include bookmarks and history, discard stale suggestions, and handle keyboard input and suggestion clicks consistently.
Co-authored-by: Codex <[email protected]>