Repository navigation
Conversation
Covers the imperative API (navigator.crossOriginStorage.requestFileHandle, hash/origins validation, the create/write/read lifecycle, FileSystemWritableFileStream's seek()/truncate()/verify-and-store behavior, idlharness), origins-based disclosure scoping across same-site/list/wildcard tiers with GREASE'ing tolerance, Permissions Policy integration, and the declarative CSS/HTML/JS integrations (cross-origin-storage() in @font-face src, the crossoriginstorage attribute on <link>/<script>, and the crossOriginStorage import attribute). https://wicg.github.io/cross-origin-storage/
…mport attribute Import attribute values must be string literals -- a SyntaxError for static import and a TypeError for dynamic import() otherwise -- so the array-valued crossOriginStorage examples ([] for same-site, [origin] for a restricted list) were invalid. Switch to the same space-separated string convention already used by the crossoriginstorage HTML attribute. The static-import-crossoriginstorage.mjs resource in particular used an array literal inside a static import's `with` clause, which would not have parsed at all.
Rephrase the "Static form smoke test" comment to describe what the test verifies rather than categorize where it came from; a scan of the rest of the suite found no other such comments.
…on "declarative" Dynamic import() is a function call, and even the static import form needs a running module graph, so grouping it with the genuinely declarative HTML/CSS integrations was misleading. Rename the declarative-js/ directory to import-attribute/ (matching the file names it already used) and update titles, test names, and comments accordingly. Generic comments that referred to all three integrations collectively now say "declarative and import-attribute" or "the other integrations" instead of lumping JS in under "declarative".
This was referenced Aug 7, 2026
Member
|
Tests for early proposals should have
|
Tests for early proposals should have .tentative in the filename per https://web-platform-tests.org/writing-tests/file-names.html.
Contributor
Author
|
Thanks @saschanaz, done — all test files now have |
tomayac
added a commit
to tomayac/firefox
that referenced
this pull request
Aug 9, 2026
Copies the cross-origin-storage/ test directory and the companion interfaces/cross-origin-storage.idl (needed by idlharness's IDL fetch) from web-platform-tests/wpt#61811. Against this branch's implementation: - idlharness, requestFileHandle-validation, requestFileHandle-create-and-read, origins-scoping: fully pass (all Window/dedicated-worker/SharedWorker/ServiceWorker globals). - filesystemwritablefilestream-verify: 4 known, already-documented Phase 1 gaps -- seek()/truncate() (not implemented; COS's own spec never uses them, only FileSystemWritableFileStream's generic surface does) and ArrayBuffer/ArrayBufferView write() chunks (e.g. from piping a fetch() body; CrossOriginStorageSinkAlgorithms currently only accepts Blob/string). - permissions-policy: 3/5 pass; the 2 failures are due to Firefox not parsing the modern `Permissions-Policy` HTTP response header at all yet (only the legacy `Feature-Policy` header name exists in-tree) -- a pre-existing platform gap unrelated to this feature. - declarative-html/declarative-css/import-attribute subdirectories are out of scope: they test separate host-language integrations (HTML/CSS/JS import attributes) that this spec explicitly delegates to their own specifications, not implemented here.
None of the existing files test that the imperative API and the declarative HTML/CSS/import-attribute integrations share one underlying COS registry, as section 8 of the spec says they do -- each existing file only proves a given mechanism is internally self-consistent (store and read back through the same mechanism). Add a new file that stores through one mechanism and reads through a different one, in both directions: imperative write -> declarative HTML read, declarative CSS write -> imperative read, JS import attribute write -> imperative read, and imperative wildcard write -> declarative HTML read back to the same (storing) document.
- A non-storer widening an entry's origins list: every existing upgrade test has the original storer perform the widening write itself, never a different origin, even though the spec's closing note in § 3.4 explicitly says any origin supplying correct bytes can do this. - origins: [] versus omitted origins: these are not equivalent (an explicit empty list authorizes nobody but the storer, not even a same-site sibling), but nothing exercised the distinction. - The single-string origins convenience form (as opposed to a one-element array) was only exercised on the validation-rejection path, never on a successful create-then-read roundtrip. - A second origin becoming a storing origin for a wildcard-scoped entry, and thereafter reading it back deterministically (GREASE-exempt) the same way the original storer already does.
Per the 'pending writer count' note in § 2.2, a create()'d handle that createWritable() is never called on still permanently counts as an outstanding writer against its entry, exactly like an abandoned in-flight write. Confirm this doesn't block a second, independent create request for the same hash from succeeding normally.
navigator.crossOriginStorage is annotated [SecureContext] but nothing verified it. Add the conventional insecure-context.any.js (no .https. in the name, so it runs over plain HTTP) asserting the attribute is absent, matching the existing pattern used by e.g. webusb/insecure-context.any.js.
§ 5.1's actual quota and maximum-origins-list-length thresholds are implementation-defined and unobservable from script, so the boundary itself (the write that trips QuotaExceededError, or the widening call that finally gets capped) can't be triggered deterministically and this file doesn't attempt to. Two 'must never fail' guarantees in the spec text are implementation-independently testable, though: that rewriting identical bytes under the same hash never consumes additional quota, and that cumulative origins-list growth across many separate writes never fails the write itself.
origins-scoping only covers the gate's negative direction, and deliberately accepts either outcome because PHL membership is implementation-defined: it stores content it invented, which cannot be on the list. A user agent that disclosed nothing at all, or one that ignored the list and disclosed everything, both pass it unchanged. Reading a listed and an unlisted hash from the same cross-site origin, at the same wildcard scope, is what makes the assertion meaningful -- neither half alone is, since each is satisfiable by a degenerate implementation. Each is read several times because GREASE'ing can suppress an authorized disclosure, so a single refusal proves nothing; an unlisted hash is refused every time. The listed resource is jQuery 3.7.1's minified build, from the list's core section: it is admitted there because several independent CDNs serve it byte-identically, which is also what makes it stable enough to pin a test to, and it is MIT-licensed so it can be redistributed here. The vendored copy has to stay byte-identical to what those CDNs serve -- adding a licence header would change its hash and silently turn this into a test of an unlisted resource, which the digest assertion guards against. Adds cosRemoteReadDigest(), because the existing helper returns text and comparing text is lossy for arbitrary stored bytes.
…ative tests
Two assertions could not fail regardless of implementation behaviour, so
they were not testing what they claimed to.
declarative-html/crossoriginstorage: a <link>'s rules keep applying for as
long as it is in the document, and t.add_cleanup() only runs once the whole
test is over. Clearing the marker's inline style does not stop an earlier
stylesheet from applying, so in the "omitting crossoriginstorage" test the
real stylesheet loaded a few lines above was still in effect and
assert_not_equals(markerColor(), 'rgb(1, 2, 3)') could never hold. The three
positive <link> tests had the same flaw in the opposite direction: their
colour assertions were satisfied by the first <link> whatever the second one
did, leaving the un-rejected load as their only real signal.
Each of these now removes the first <link> before loading the second, and
asserts the marker actually resets, so the following assertion is
demonstrably capable of failing.
declarative-css/cross-origin-storage: promise_rejects_js() requires a JS
Error subtype and rejects the assertion on its own argument before ever
evaluating the promise ("function DOMException() { [native code] }" is not
an Error subtype). document.fonts.load() rejects with a DOMException, so
this needs promise_rejects_dom(). The name is normative: CSS Font Loading
rejects a FontFace's [[FontStatusPromise]] with a "NetworkError" DOMException
when the resource fails to load, and FontFaceSet.load() propagates that
rejection. ("SyntaxError" applies only to ArrayBuffer sources, which these
tests do not use.)
tomayac
force-pushed
the
cross-origin-storage
branch
from
August 15, 2026 21:05
7f695fd to
19a45a9
Compare
A FileSystemFileHandle is serializable, so postMessage() is a second way to obtain a handle for a COS entry alongside requestFileHandle(), and the disclosure controls have to hold across it. Nothing covered that. Adds five subtests: a readable handle survives a transfer to a same-origin worker and to a same-origin frame with its bytes intact; a create-request handle that was never written through does not become readable by being transferred; readability is captured when the handle is serialized rather than re-evaluated on arrival (re-evaluating would let a caller re-roll GREASE'ing by transferring one handle repeatedly); and a handle posted to a cross-origin frame is not delivered. That last one restates a guarantee /fs already tests generically, but COS leans on it for a disclosure property the /fs tests aren't concerned with, so a regression should fail this suite too.
phl-listed-resource.js is jQuery 3.7.1 verbatim, because the test needs a resource the Public Hash List actually lists. The minified bundle contains setTimeout and console.* calls, which lint flags, failing CI on a file that isn't ours to edit. Ignores both, alongside the existing entry for annotation-model's ajv.min.js.
isSameEntry() and createWritable({keepExistingData: true}) were both marked
SPEC GAP, with the reasoning carried in the test files because there was nowhere
else for it to live. The specification now covers both, so cite it instead and
drop the claim that these are undecided.
The reasoning stays in keep-existing-data, since it is what the assertions
actually check. handle-surface's remaining gaps -- kind, name, move(), remove()
and createSyncAccessHandle() -- are still marked as such.
…Handle() Not a spec gap: createSyncAccessHandle() rejects with an "InvalidStateError" whenever the handle is not in a bucket file system, and the COS file system is a root distinct from any origin's, so the File System Standard already fixes both the refusal and its name. The test had guessed NotSupportedError and named InvalidStateError only as the plausible alternative. It is the required one.
name, move(), remove() and the permission queries are all decided now, so cite the specification instead of recording a choice this file had to make. move() and remove() move from NotSupportedError to NotAllowedError -- the alternative the remove() note already argued was stronger -- and the permission test gains the readwrite case it had deliberately left unpinned.
…isibility Cover three recently specified behaviors: - Cross-Origin-Storage-Allow-Origin header (allow-origin-header.tentative): a non-same-site origins list is intersected with the writing document's ceiling header, so an origin the header omits is dropped and refused deterministically, a write with no header discloses to no cross-site origin, being named by the header is a bound rather than a grant, and a "*" write needs no header. List-scoped writes run in a RemoteContext whose response carries the ceiling via pipeHeader, since the top-level document cannot set a header on itself. - Permissions Policy over workers (permissions-policy-workers.tentative): the check resolves through a worker's owner set, so a blob: worker cannot escape a cross-origin-storage=() policy on the page that created it, the check still precedes hash validation in a worker, and a service worker (empty owner set) fails closed. - Additive resource visibility (visibility-upgrade-list-preserved.tentative): a later "*" write adds the global grant without discarding an existing origins list, so a listed origin keeps its PHL-independent access; the same-site tier likewise survives a "*" write; and a storing origin keeps deterministic access. Because a clobbering implementation would turn every post-upgrade listed read into a permanent (not GREASE'd) NotFound for content never on the PHL, the file's meta assertion fails loudly on regression rather than passing vacuously. Adds cosRemoteStore() and cosAllowOriginPipe() helpers and a service worker resource.
… list writes A non-same-site origins list is only honored for origins the writing document's Cross-Origin-Storage-Allow-Origin header authorizes, so the list-scoped tests here were storing against a ceiling they never set. Add a companion .sub.headers whose ceiling names every origin in the cross-origin pools (verified to resolve to exactly cosOriginPools()'s set), so each pool origin a test lists is authorized. The one list-scoped write made from a remote context (the non-storer-widening test) instead carries its own ceiling via pipeHeader, since the ceiling is the writer's, not this document's.
# Conflicts: # interfaces/cross-origin-storage.idl
…workers `global=worker` expands to dedicated, shared, and service workers, so these six files expected requestFileHandle() to succeed in a ServiceWorkerGlobalScope. That contradicts the spec and permissions-policy-workers.tentative.https.html: a service worker's owner set is empty, so the Cross-Origin Storage permission check fails closed and every call rejects with NotAllowedError. No implementation could pass both. Run them in windows, dedicated workers, and shared workers instead, as insecure-context.tentative.any.js already does. The service worker case stays covered by permissions-policy-workers.
The spec made resource visibility strictly additive: every entry grants its storing origins and the origins same site with them, whatever its explicit origins list holds. An explicit empty list therefore grants exactly what an omitted origins value does. This test still encoded the earlier model, where origins could be null and an empty list withheld the entry even from a same-site sibling. Expect the sibling to be authorized (subject to GREASE'ing) instead. See WICG/cross-origin-storage#80.
… css Match the explainer's section names and the sibling import-attribute and fetch directories, and point the explainer links at the renamed anchors.
A create request registers nothing in the COS registry, so a hash whose only write is still in flight now reads as an ordinary NotFoundError. It previously rejected with a distinguishable NotAllowedError. That distinguishable answer handed any origin one noiseless bit about any hash, decided ahead of origins scoping, the Public Hash List, and GREASE'ing, and without writing any bytes for the storage limit to bound. An abandoned create handle also pinned its hash indefinitely, suppressing every other origin's reads of it. - requestFileHandle-create-and-read: flip the concurrent-read and abandoned-handle assertions to NotFoundError, and drop the placeholder cleanup rationale from the hash-mismatch tests. - probe-budget: remove the pending-writer budget exemption, which no longer has anything to exempt. - origins-scoping: add a regression test asserting that a cross-site origin gets the same answer for a hash with an in-flight write as for one that was never written. getFile() on an unwritten create handle still rejects with NotAllowedError. That gate is per handle, its answer is constant with respect to the registry, and a handle cannot be deserialized cross-origin, so it crosses no origin boundary.
tomayac
added a commit
to WICG/cross-origin-storage
that referenced
this pull request
Sep 23, 2026
`complete a create request` no longer reads or writes the COS registry. An entry is added only by `verify and store` or by `store a fetched resource in Cross-Origin Storage`, in both cases after the complete bytes have been verified against the hash. The read path loses its pending branch, so `apply availability gating` is the first and only step that decides a read. Previously a create request registered a placeholder entry, and a concurrent read of it rejected with a distinguishable `NotAllowedError`, so that a caller would not start a redundant multi-gigabyte download. That answer was available to any origin, for any hash, with no GREASE'ing applied and without writing any bytes, so it walked around `origins`, the Public Hash List, and GREASE'ing together, and neither the storage limit nor eviction bounded it. An abandoned create handle also pinned its hash in that state indefinitely, which suppressed every other origin's reads of it. - Drop `state` and `pending writer count` from the COS entry struct, and make `bytes` non-null. A registry entry is written by construction. - Give `FileSystemFileHandle` a `COS hash` item, so a handle addresses a hash and needs no entry to exist. Serialize it alongside `COS origin`. - Simplify `verify and store`: a failed write has nothing to clean up. - Add an "In-progress writes" section recording the invariant and the reasons the placeholder was removed. `getFile()` on an unwritten create handle still rejects with `NotAllowedError`. That gate is per handle, its answer does not vary with the registry's contents, and a handle cannot be deserialized cross-origin. Tests: web-platform-tests/wpt#61811
Follows WICG/cross-origin-storage#86, which renames the method, its CrossOriginStorageGetFileHandleHash and CrossOriginStorageGetFileHandleOptions dictionaries, and the spec anchor.
…7151e9c Reads by storing, listed, and same-site origins are now asserted strictly with `cosAssertDisclosed()`, since the spec forbids GREASE'ing them and limits its lookup budget and user-gesture gate to the "*" grant. List-tier tests list a cross-site origin, integration tests take the scope ceiling from the fetched resource's own Cross-Origin-Storage-Allow-Origin header, and every integration test stores bytes unique to the run. Named origins must be potentially trustworthy http(s) origins, SHA-1 is rejected, and createWritable() and isSameEntry() on COS handles reject with NotAllowedError. The fetch tests no longer run in service workers, which fail the permission check. Font probes check each face's status, since `document.fonts.check()` reports success for a face that failed to load. Tests for behavior the spec leaves to implementations or to host specifications move to .optional files: the lookup budget, Public Hash List membership, fetch Response details, and queryPermission() and requestPermission(). New tests cover worker ceilings, header parsing, same-site extension of listed origins, additive grants, handle transfer, algorithm validation, and the service worker fetch path.
The HTML integration tests loaded classic scripts and stylesheets inserted by script. Adds coverage for the rest of what the crossoriginstorage attribute applies to: - module scripts, and <link rel=modulepreload>, whose preloaded module an import() of the broken URL then resolves to - <link rel=preload> as script and as style, plus a preload without the attribute that must not be served from COS - <link> and <script> written in markup and loaded by the parser - crossoriginstorage without integrity, which has nothing to look up and so has no effect
The CSS integration tests loaded fonts through @font-face. Adds coverage for the rest of what cross-origin-storage() applies to: - parsing and serialization against the proposed grammar, cross-origin-storage( [ '*' | <string># ]? ), including arguments the grammar rejects, which invalidate the declaration as an unknown modifier does - @import, with the same-site default and with * - images, observed through the size a content: url() image gives its box, plus an image without the modifier that must not be served from COS cos-resource.py gains kind=image, which serves a PNG with unique trailing bytes, the way kind=font does for Ahem. The two cross-origin integration tests open several remote contexts, so they are marked as long-running.
An image sizes its box as soon as its header is decoded, before its load finishes and its bytes are stored, so the CSS image test now waits for the entry itself before requesting it from a URL that 404s. The tests that open several cross-site contexts or workers take well over the default timeout under load, so they are marked as long-running.
The test that a list refuses an unlisted cross-site origin listed one cross-site pool origin and read from another. Every origin in that pool belongs to one site, and a listed origin admits the origins same site with it, so the reader was admitted. It now lists an origin of the document's own site and reads from the cross-site pool, as origins-scoping does. Adds a test that crossOriginStorage on a Request object takes part in COS, also once the Request is copied, and marks the Permissions Policy fetch test, which opens remote contexts, as long-running.
An engine reads only the RequestInit members it supports, so a getter on the init dictionary detects crossOriginStorage, as for other members such as duplex. Feature detection relies on this; the member is not reflected on Request.
The broken-URL specifiers in the import attribute tests lacked a './' prefix, which made them bare specifiers that fail to resolve before any fetch, so the tests could not reach Cross-Origin Storage. New tests cover a static import of a broken URL served from COS, a static import with integrity alone that gets no COS participation, and JSON and CSS modules stored into and served from COS.
A frame reads back what it wrote only under the same top-level site, an origin's top-level writes don't reach its frames elsewhere, the same-site baseline is keyed too, a first-party origins list admits the listed origins first-party and under the writer's top-level site, and a list written from a cross-site frame stays under that frame's top-level site. A create request with no bytes doesn't make its caller a storing origin.
This branch has not been deployed
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.
This PR covers both the imperative API (
navigator.crossOriginStorage) and the additional integrations for HTML, CSS, and JS.