Cross-Origin Storage

Draft Community Group Report,

This version:
https://wicg.github.io/cross-origin-storage/
Issue Tracking:
GitHub
Inline In Spec
Editors:
(Google)
(Thinktecture AG)
(Google)
Participate:
GitHub WICG/cross-origin-storage (new issue, open issues)
Commits:
GitHub index.bs commits

Abstract

Cross-Origin Storage (COS) is a content-addressable cache that allows web applications to store and retrieve large files, such as AI models, WebAssembly modules, and highly popular JavaScript libraries, across different origins. Files are identified by their cryptographic hash rather than by URL, so a byte-identical resource fetched once by one origin can be reused by any other origin the user agent permits, without a second download. To keep cache presence from being usable to infer which sites a user has visited, cross-origin disclosure of a file is limited to hashes that clear a k-anonymity-style popularity bar.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

Storage is normally partitioned by origin to protect user security and privacy. That is the right default, but it is a poor fit for a narrow class of very large, highly popular, publicly-distributed resources — AI models, WebAssembly modules, JavaScript libraries, game engines, and large web fonts — that are, byte-for-byte, the same file no matter which site requested them. When two unrelated origins each depend on the same 8 GB model, origin-partitioned storage forces the user to download and retain that model twice, which is wasteful for the user’s bandwidth, storage, and battery, and for the network as a whole.

Cross-Origin Storage (COS) is a content-addressable cache, keyed by cryptographic hash rather than by URL, that lets a user agent share a single stored copy of such a resource across the origins that opt into using it. Storing a resource in COS is always an explicit, opt-in act by the storing origin, and a user agent may withhold confirming a resource’s presence even from origins that would otherwise be permitted to read it; see § 7 Privacy and security considerations.

The entry point for this specification is the requestFileHandle() method, exposed via crossOriginStorage:

const hash = {
  algorithm: 'SHA-256',
  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};
try {
  const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
  const file = await handle.getFile();
  // Do something with the file.
} catch (err) {
  if (err.name === 'NotFoundError') {
    // Not (disclosably) in Cross-Origin Storage; fetch from the network instead.
  }
}

This specification reuses FileSystemFileHandle, FileSystemWritableFileStream, and related infrastructure from the File System Standard [FS], scoped to a dedicated, cross-origin-shared file system rather than to a single origin’s private or user-visible file system.

This specification defines the imperative JavaScript API only. Related proposals integrate the same underlying cache into other host specifications — an HTML crossoriginstorage attribute on link and script, a crossOriginStorage JavaScript import attribute, a CSS cross-origin-storage() <request-url-modifier>, and a crossOriginStorage RequestInit option for fetch() — each defined in its own host specification; see § 8 Integration with other specifications.

2. Concepts

2.1. Hashes

A COS hash is a struct with the following items:

algorithm

A string naming a hash algorithm recognized by [WEBCRYPTO], for example "SHA-256".

value

A string that is a lowercase hexadecimal encoding of a digest produced by algorithm. For "SHA-256", this is 64 characters long.

The algorithm dictionary member is typed as a plain DOMString, not as a HashAlgorithmIdentifier, even though its value space is exactly the set of names accepted by [WEBCRYPTO]’s hash algorithms. A HashAlgorithmIdentifier is (object or DOMString) — it additionally admits an object, normalized by [WEBCRYPTO] into an Algorithm-shaped dictionary, so that callers can pass extra parameters to algorithms that need them (for example, {name: "HMAC", hash: "SHA-256"}). Hash algorithms take no such parameters, algorithm is stored, compared, and round-tripped as part of a content-addressable key rather than consumed once by a single operation, and every use in this specification’s examples is a bare string; admitting arbitrary objects here would add no capability while making equality and serialization needlessly complicated. DOMString is therefore the narrower and more correct type for this field.

Two COS hash values are equal if their algorithm values are an ASCII case-insensitive match and their value values are exactly equal.

Note: Unlike algorithm, whose case is not normatively constrained, value is already normatively lowercase (see above), so comparing it is a plain string comparison rather than an ASCII case-insensitive one.

A content-addressable store is one whose entries are keyed by the equality of a COS hash, rather than by URL or by name: two files with identical bytes and the same hash algorithm are the same entry, regardless of how many origins stored them or how many URLs they were fetched from.

2.2. COS entries

Each user agent has a single COS registry, a map from COS hash values to COS entry structs, shared by all origins that use Cross-Origin Storage. A COS entry has the following items:

hash

A COS hash.

bytes

Null, or a byte sequence whose digest under hash’s algorithm equals hash’s value. Null until a writer completes verification and storage.

state

Either "pending" or "written". An entry starts out "pending" and becomes "written" exactly once, when bytes is first set.

pending writer count

A non-negative integer, initially 0, counting outstanding writers — FileSystemFileHandle handles returned by complete a create request whose closing write has not yet settled. Used only to decide, in verify and store, whether a failed write is safe to clean up; see § 3.3 Creating and writing files.

origins

The declared sharing scope: either "*" (any origin), a list of origin strings (only those origins), or null (same-site origins only). Set by the first writer and upgradeable but never downgradeable.

storing origins

A set of origins that have each successfully completed writing this entry’s bytes at least once. Persisted across page loads and grows over time; never shrinks except as described in § 5.2 Eviction. An origin in storing origins may always obtain a handle for the entry via requestFileHandle(), independent of origins or whether the entry’s hash is on the Public Hash List (PHL). See § 3.4.1 Original storer access.

A user agent has an associated Cross-Origin Storage queue, which is the result of starting a new parallel queue. All operations on the COS registry are to be enqueued on this queue, so that they execute in the order they were enqueued and do not interleave.

2.3. The Public Hash List

Confirming that a hash is present in Cross-Origin Storage can itself leak information about a user’s browsing history (see § 7.2 Cross-site probing). This is a risk specifically for globally scoped entries — an entry whose origins is a list or null has already had its disclosure bounded by an explicit choice of the storing origin, but "*" potentially exposes an entry to any origin on the web. To bound that specific risk, a "*"-scoped resource is only disclosable to origins outside its storing origins if its hash clears an additional, independent gate: membership on the Public Hash List (PHL), a vendor-neutral, implementation-defined allowlist of COS hash values. A hash is admitted to the Public Hash List only once it clears a k-anonymity-style popularity bar — for example, appearing, byte-identical, across some minimum number of independent origins — so that confirming its presence in a shared cache is not informative about any individual user.

A COS hash hash is on the Public Hash List if hash equals an entry of the user agent’s current snapshot of the Public Hash List.

The retrieval protocol, update cadence, data format, popularity thresholds, and governance of the Public Hash List are designed in detail as a companion artifact to this specification, in the spirit of how the [URL] Standard’s public suffix list is a companion data file rather than normative prose — see the Public Hash List explainer in this repository. That design proposes governance by the WHATWG, modeled on the Public Suffix List’s cross-vendor, rolling-release precedent, and admission criteria keyed on independently corroborated ubiquity —​a concrete instance of the k-anonymity-style bar described above, applied once, offline, as part of how a hash is admitted, not as a check the user agent repeats per query. None of this is yet established outside this proposal. An early, non-normative code prototype of the list itself is maintained in this repository, at public-hash-list/implementation/ — a pragmatic interim home; a dedicated, cross-vendor repository remains the governance target described in the PHL explainer. This specification only depends on whether a hash is on the PHL, not on any particular retrieval mechanism or governance model, so it remains correct regardless of how that design evolves.

3. The CrossOriginStorageManager interface

[Exposed=(Window,Worker), SecureContext]
interface CrossOriginStorageManager {
  Promise<FileSystemFileHandle> requestFileHandle(
      CrossOriginStorageRequestFileHandleHash hash,
      optional CrossOriginStorageRequestFileHandleOptions options = {});
};

dictionary CrossOriginStorageRequestFileHandleHash {
  required DOMString value;
  required DOMString algorithm;
};

dictionary CrossOriginStorageRequestFileHandleOptions {
  boolean create = false;
  (DOMString or sequence<DOMString>) origins;
};

interface mixin NavigatorCrossOriginStorage {
  [SameObject, SecureContext] readonly attribute CrossOriginStorageManager crossOriginStorage;
};
Navigator includes NavigatorCrossOriginStorage;
WorkerNavigator includes NavigatorCrossOriginStorage;

Each Navigator and WorkerNavigator object has an associated CrossOriginStorageManager object. The crossOriginStorage getter steps are to return this’s associated CrossOriginStorageManager.

Each CrossOriginStorageManager object has an associated origin, set to this’s relevant settings object’s origin when the object is created.

Note: Taking the origin from the relevant settings object rather than from anything about the script itself has a consequence worth spelling out for workers. A worker created from a blob: URL has the origin of the context that created it, so it shares that origin’s Cross-Origin Storage view exactly: what such a worker writes, its creating page can read back, and what the page stored, the worker can read. The blob: URL is not an origin of its own, and revoking it does not detach the worker from that view. A worker created from a data: URL, by contrast, has an opaque origin. This specification does not currently define what requestFileHandle() does when the calling origin is opaque; the opaque origin rule in validate a COS request governs the origins a caller names, not the origin it speaks from. An opaque origin has no stable identity to key storing origins or same site comparisons on, so there is nothing meaningful for it to be granted.

3.1. The requestFileHandle() method

handle = await navigator . crossOriginStorage . requestFileHandle(hash)

Returns a handle for the file identified by hash, if it is present in, and disclosable from, Cross-Origin Storage to the calling origin. Otherwise, this rejects with a "NotFoundError" DOMException. A "NotFoundError" does not prove the file is physically absent from Cross-Origin Storage; see § 7.3 Availability gating. Callers can treat it as "fetch this from the network instead."

handle = await navigator . crossOriginStorage . requestFileHandle(hash, { create: true })

Returns a handle that can be used to write the file identified by hash into Cross-Origin Storage, creating a new entry if none exists yet, restricted by default to same-site origins. The caller needs to write the complete file contents via the handle’s createWritable() method regardless of whether an entry already existed; the user agent verifies at that point that the written bytes hash to hash, rejecting with a "DataError" DOMException otherwise. This requirement prevents an origin from using a create request as an oracle for whether hash was already present.

handle = await navigator . crossOriginStorage . requestFileHandle(hash, { create: true, origins: "*" })

As above, additionally making the entry disclosable to any origin whose request for hash clears § 7.3 Availability gating, once written.

handle = await navigator . crossOriginStorage . requestFileHandle(hash, { create: true, origins: ["https://a.example", "https://b.example"] })

As above, restricting disclosure to exactly the listed origins (in addition to the calling origin, and any origin already in storing origins).

The requestFileHandle(hash, options) method steps are:
  1. Let result be a new promise.

  2. Let realm be this’s relevant Realm.

  3. Let global be this’s relevant global object.

  4. Let origin be this’s associated origin.

  5. If global’s associated Document, if any, is not allowed to use the "cross-origin-storage" policy-controlled feature, then:

    1. Queue a global task on the DOM manipulation task source given global to reject result with a "NotAllowedError" DOMException.

    2. Return result.

  6. Let validationFailure be the result of running validate a COS request given hash and options.

  7. If validationFailure is not null:

    1. Queue a global task on the DOM manipulation task source given global to reject result with validationFailure.

    2. Return result.

  8. Enqueue the following steps to the Cross-Origin Storage queue:

    1. If options["create"] is true:

      1. Run complete a create request given result, hash, options, global, and realm.

    2. Otherwise:

      1. Run complete a read request given result, hash, origin, global, and realm.

  9. Return result.

To validate a COS request given a CrossOriginStorageRequestFileHandleHash hash and a CrossOriginStorageRequestFileHandleOptions options, return null or a TypeError:
  1. If hash["algorithm"] is not a hash algorithm name recognized by [WEBCRYPTO], return a new TypeError.

  2. If hash["value"] does not match the regular expression /^[0-9a-f]{64}$/ when hash["algorithm"] is an ASCII case-insensitive match for "SHA-256", return a new TypeError.

    Note: Future hash algorithms can define a different expected digest length; this specification only normatively constrains "SHA-256", matching its use throughout the examples.

  3. If options["origins"] exists and is not "*":

    1. Let candidates be options["origins"], with a single string treated as a list of one.

    2. If candidates’s size is greater than the user agent’s maximum origins list length, return a new TypeError.

    3. For each candidate of candidates:

      1. If the result of running the basic URL parser on candidate is failure, or if the parsed URL’s origin is an opaque origin, return a new TypeError.

  4. Return null.

3.2. Reading files

To complete a read request given a promise result, a COS hash hash, an origin origin, a global object global, and a Realm realm:
  1. Let entry be the COS entry in the COS registry whose hash equals hash, if any, or null otherwise.

  2. If entry is not null and entry’s state is "pending":

    1. Queue a global task on the DOM manipulation task source given global to reject result with a "NotAllowedError" DOMException.

    2. Return.

    Note: A hash with a write in progress deliberately does not behave like a hash that is absent, so that a caller does not mistake an in-progress write for a genuine cache miss and begin a redundant, concurrent download of a very large file for the sole purpose of writing it. See § 3.3 Creating and writing files.

  3. Let disclosableEntry be the result of running apply availability gating given entry and origin.

  4. If disclosableEntry is null:

    1. Queue a global task on the DOM manipulation task source given global to reject result with a "NotFoundError" DOMException.

    2. Return.

  5. Let handle be the result of creating a new FileSystemFileHandle [FS] whose locator addresses disclosableEntry within the Cross-Origin Storage file system, in realm.

  6. Set handle’s COS origin to origin.

  7. Set handle’s may read to true.

    Note: This handle has already cleared apply availability gating, so the calling origin is entitled to the entry’s bytes.

  8. Queue a global task on the DOM manipulation task source given global to resolve result with handle.

To determine COS disclosure given a COS entry or null entry, and an origin origin, return a COS entry or null:
  1. If entry is null, return null.

  2. Assert: entry’s state is "written".

  3. If origin is in entry’s storing origins, return entry.

    Note: The original storer, and any origin that has itself successfully written the entry, can always read it back. See § 3.4.1 Original storer access.

  4. If entry’s origins is "*":

    1. If entry’s hash is not on the PHL, return null.

      Note: The Public Hash List gate applies only here, to globally-scoped entries — the one case where disclosure could otherwise reach any origin on the web. It does not apply to the list- and same-site-scoped cases below: a storing origin has already made an explicit, bounded disclosure decision for those, and requiring separate global ubiquity on top of it would make ordinary restricted sharing (see § 3.4 Resource visibility upgrades) depend on unrelated, public curation of what is often a proprietary resource. See § 7.2 Cross-site probing.

    2. Return entry.

  5. If entry’s origins is a list:

    1. If origin’s serialization is an item of that list, return entry.

    2. Return null.

  6. Assert: entry’s origins is null.

  7. If origin is same site with some item of entry’s storing origins, return entry.

  8. Return null.

To apply availability gating given a COS entry or null entry, and an origin origin, return a COS entry or null:
  1. Let disclosable be the result of running determine COS disclosure given entry and origin.

  2. If disclosable is null, return null.

  3. If origin is in disclosable’s storing origins, return disclosable.

  4. If the user agent elects to apply GREASE’ing to this request, return null.

  5. Return disclosable.

3.3. Creating and writing files

A FileSystemFileHandle created by complete a create request has an associated requested origins, the value that verify and store will attempt to upgrade the entry’s origins to once the handle is successfully written through.

Every FileSystemFileHandle addressing a COS entry also has an associated COS origin, the origin that obtained it, and an associated may read boolean, which governs whether getFile() may disclose the entry’s bytes through that particular handle. It is true for a handle returned by complete a read request, which has already passed availability gating, and false for a handle returned by complete a create request until verify and store succeeds for that same handle.

Note: This is deliberately a property of the handle rather than of the entry. A create request returns a handle whether or not the entry already exists and is already "written" (see complete a create request), so a per-entry rule would let any origin call requestFileHandle() with create: true and immediately read an entry it never wrote, learning its contents without satisfying origins, PHL membership, or GREASE’ing. Every disclosure control in this specification is applied on the read path, so gating per entry would let a create request walk around all of them. Requiring the caller to supply the correct bytes first means a successful read through a create-obtained handle discloses nothing the caller did not already have.

To complete a create request given a promise result, a COS hash hash, a CrossOriginStorageRequestFileHandleOptions options, a global object global, and a Realm realm:
  1. Let requestedOrigins be the result of running normalize requested origins given options["origins"].

  2. Let entry be the COS entry in the COS registry whose hash equals hash, if any, or null otherwise.

  3. If entry is null:

    1. Set entry to a new COS entry whose hash is hash, bytes is null, state is "pending", origins is requestedOrigins, and storing origins is an empty set.

    2. Set the COS registry[hash] to entry.

  4. Increment entry’s pending writer count by 1.

    Note: This tracks handle as an outstanding writer even if the caller never calls createWritable() on it, or never closes the resulting stream. Such an abandoned handle permanently counts against entry, exactly as an abandoned in-flight write already left entry permanently "pending" before this mechanism existed; see verify and store.

  5. Let handle be the result of creating a new FileSystemFileHandle [FS] whose locator addresses entry within the Cross-Origin Storage file system, in realm.

  6. Set handle’s COS origin to global’s relevant settings object’s origin.

  7. Set handle’s requested origins to requestedOrigins.

  8. Set handle’s may read to false.

    Note: handle is returned whether or not entry already existed, and whether or not it is already "written". The caller still needs to supply the complete file bytes through handle, so that a write cannot be used to detect prior presence (see § 7.2 Cross-site probing) and so that a request for a more permissive origins value can be verified before being honored; see § 3.4 Resource visibility upgrades.

  9. Queue a global task on the DOM manipulation task source given global to resolve result with handle.

To normalize requested origins given a (DOMString or sequence<DOMString>) or undefined origins, return "*", a list of origins, or null:
  1. If origins does not exist, return null.

  2. If origins is "*", return "*".

  3. Let list be « ».

  4. For each candidate of origins (a single string is treated as a list of one):

    1. Let candidateOrigin be the origin resulting from running the basic URL parser on candidate.

      Note: validate a COS request has already confirmed each candidate parses to a non-opaque origin.

    2. If list does not contain candidateOrigin, append candidateOrigin to list.

  5. Return list.

Note: Deduplicating here, rather than only when later merging into an existing entry, keeps origins duplicate-free from the moment an entry is first created, so every later clone of it stays duplicate-free too.

When the FileSystemWritableFileStream obtained by calling createWritable() on a handle whose locator addresses a COS entry is closed [FS], the user agent must run the verify and store steps below before that closing operation’s promise is fulfilled.

Note: This applies regardless of how closing was triggered — an explicit close() call, or a pipeTo() call that reaches the end of its source, which by default also closes its destination. [STREAMS] does not distinguish the two at the point a stream becomes closed, and neither does this algorithm.

The verify and store steps, given the COS entry entry addressed by the closed stream’s handle, the complete written byte sequence bytes, and the closing operation’s relevant settings object’s origin origin, are:
  1. Let computedValue be the lowercase hexadecimal digest of bytes, computed using the algorithm named by entry’s algorithm, per [WEBCRYPTO].

  2. If computedValue is not exactly equal to entry’s value:

    1. Reject the closing operation’s promise with a "DataError" DOMException.

    2. Enqueue the following steps to the Cross-Origin Storage queue:

      1. Decrement entry’s pending writer count by 1.

      2. If entry’s state is "pending" and entry’s pending writer count is 0, then remove entry’s hash from the COS registry.

        Note: This is what makes a failed write clean up after itself instead of leaving a permanently unreachable "pending" entry behind: a later requestFileHandle() call for the same hash, once no other writer is still outstanding, sees no entry at all and gets an ordinary "NotFoundError", not an indefinite "NotAllowedError". The count guards against deleting an entry that another writer, concurrently in flight for the same hash (for example, two tabs that both found the hash absent and independently began writing it), might still successfully write: if that writer is still outstanding, or already succeeded, either the count has not yet reached 0 or entry’s state is no longer "pending", and this step is a no-op. An already-"written" entry can never be removed this way, regardless of how many later writers request it and fail: only an entry that has never once been successfully written is eligible.

    3. Abort these steps.

  3. Enqueue the following steps to the Cross-Origin Storage queue:

    1. Set entry’s bytes to bytes.

    2. Set entry’s state to "written".

    3. Append origin to entry’s storing origins, if not already present.

    4. Decrement entry’s pending writer count by 1.

    5. Run upgrade resource visibility given entry and the handle’s requested origins.

    6. Set the handle’s may read to true.

      Note: Only this handle becomes readable. Another handle for the same entry, obtained from a different create request that has not been written through, stays unreadable.

createWritable() called on a FileSystemFileHandle addressing a COS entry must produce a FileSystemWritableFileStream whose contents start empty, regardless of the value of keepExistingData and regardless of the addressed entry’s state.

Note: [FS] otherwise defines keepExistingData as seeding the stream with the existing file’s contents. Honoring that here would let a caller become a storing origin for bytes it never possessed. A create request returns a handle whether or not the entry already exists, so a caller could open a writable for a hash another origin stored, close it having written nothing, and have the carried-over bytes hash to the requested value. That would grant it read access with origins, the Public Hash List and GREASE’ing never consulted — an availability gating bypass assembled out of two individually permitted operations. It is the same reasoning that makes verify and store require the complete contents on every create request.

Note: A writable closed without any write therefore holds the empty byte sequence, which will not hash to the requested value for any entry a caller would want, so verify and store rejects with a "DataError" DOMException as it does for any other mismatch. An implementation that instead reports a storage or I/O failure for that case hides a verification result behind an unrelated error.

The File System Standard [FS] does not yet define an extension point for a dependent specification to hook additional per-write validation into FileSystemWritableFileStream’s closing steps. Until such a hook exists, this section describes the required behavior directly; a future revision is expected to formally integrate with [FS].

3.4. Resource visibility upgrades

The visibility of a COS entry can be upgraded but never downgraded.

To upgrade resource visibility given a COS entry entry and "*", a list of origins, or null requestedOrigins:
  1. If requestedOrigins is null, return.

    Note: Omitting origins never narrows an existing entry; it only requests same-site availability, which every entry already has at least as much of.

  2. If entry’s origins is "*", return.

    Note: An already-globally-available entry cannot be restricted by a later writer. When this note applies and requestedOrigins is not "*", the user agent is expected to log a console warning informing the developer that the requested restriction was not applied.

  3. If requestedOrigins is "*":

    1. Set entry’s origins to "*".

    2. Return.

  4. If entry’s origins is null:

    1. Set entry’s origins to requestedOrigins.

    2. Return.

  5. Let merged be a clone of entry’s origins.

  6. For each candidateOrigin of requestedOrigins:

    1. If merged contains candidateOrigin, then continue.

    2. If merged’s size equals the user agent’s maximum origins list length, then break.

      Note: Any remaining candidateOrigin values are silently dropped from this upgrade rather than failing it — the write itself already succeeded — but the user agent is expected to log a console warning that the entry’s origins list is at capacity.

    3. append candidateOrigin to merged.

  7. Set entry’s origins to merged.

Note: A new site — not only the original storer — can widen an entry’s origins, as long as it also supplies bytes that hash to the entry’s hash. This is intentional: any site that already possesses the correct bytes for a hash is, by construction, as authoritative as the original storer about what that hash denotes.

3.4.1. Original storer access

An origin that has successfully completed writing a COS entry — that is, any origin in the entry’s storing origins — can always subsequently obtain a handle for it via requestFileHandle(), independent of the entry’s origins value and independent of whether its hash is on the PHL. This mirrors the Cache API’s model, in which an origin always has access to what it stored.

4. The Cross-Origin Storage file system

The Cross-Origin Storage file system is a file system root, distinct from any origin’s bucket file system or local file system access roots, whose entries correspond one-to-one with the COS registry’s COS entry items.

The Cross-Origin Storage file system does not use [FS]’s ordinary per-call permission-check model: every FileSystemFileHandle obtained from it has already been fully authorized by § 3.2 Reading files or § 3.3 Creating and writing files before it is returned to script, so calling getFile() or createWritable() on such a handle never triggers an additional permission prompt.

queryPermission() and requestPermission() on such a handle must therefore never report "prompt", since no prompt can appear. They must report "denied" for a write mode on a handle that was not obtained by a create request, and "granted" otherwise.

Note: Writing is the one capability a handle can genuinely lack. Only a create request yields a handle that can be written through, so reporting "granted" for a write mode on any other handle would claim a capability createWritable() will refuse. Reading stays "granted", since a caller holding a readable handle can read through it. Requesting cannot change either answer, as there is no prompt to show and no state a grant could be recorded in.

Calling getFile() on a FileSystemFileHandle addressing a COS entry whose may read is false must reject with a "NotAllowedError" DOMException. This covers two distinct cases:

Once a handle’s may read is true, getFile() called on it returns a File whose contents are the addressed entry’s bytes; this is exactly the behavior [FS] already defines for a file entry whose binary data is those bytes.

A FileSystemFileHandle addressing a COS entry has a file system locator whose path is a list containing a single string, the entry’s COS hash’s value, and whose root is the Cross-Origin Storage file system.

Note: [FS] defines name as the last path component of the handle’s locator’s path, so this settles name rather than leaving it to be chosen: it is the hash. An entry has no name of its own, and the hash is the only identity it has, so reporting it carries information the empty string would discard.

An entry has no name and no containing directory, so most of [FS]’s structural surface has nothing to act on. Identity is the exception, and it is the one question a content-addressed entry can always answer: the COS registry holds at most one COS entry per COS hash, so two handles address the same entry exactly when their hashes are equal.

isSameEntry() called on a FileSystemFileHandle addressing a COS entry, with an argument that also addresses a COS entry and was obtained by the same origin, must return true if the two handles' COS hash items are equal, and false otherwise.

Note: Refusing this would discard information the user agent already holds.

isSameEntry() called with an argument the calling origin did not obtain, or one addressing a file system other than the Cross-Origin Storage file system, must reject.

Note: Returning false in that case would assert that the two handles address different entries, which is a claim about a handle the calling origin is not entitled to inspect. Rejecting says only that the comparison could not be made.

move() and remove() called on a FileSystemFileHandle addressing a COS entry must reject with a "NotAllowedError" DOMException, and must leave the entry unchanged.

Note: An entry is shared by every origin in its storing origins, so honoring a removal would let one site destroy data other sites depend on; deletion belongs to eviction and to the user’s own storage controls. A rename or a reparent has nothing to act on, since an entry is named by its COS hash and has no containing directory.

Note: Neither method is defined by [FS] or by the File System Access API at the time of writing (WICG/file-system-access#214), so the error name follows removeEntry(), the operation [FS] does define for deleting an entry: it rejects with the access result’s error name when readwrite access is not granted. Refused rather than absent is the accurate reading here — the operations are meaningful and denied.

createSyncAccessHandle() needs no rule here: its steps reject with an "InvalidStateError" DOMException whenever the handle is not in a bucket file system, and the Cross-Origin Storage file system is a file system root distinct from any origin’s, so [FS] already determines both the refusal and its name.

Note: That is also the outcome this specification would want independently. A FileSystemSyncAccessHandle hands the caller a writable file descriptor, which would let it change an entry’s bytes out from under the COS hash they are stored against, and every other origin the entry is disclosable to reads those same bytes.

4.1. Transferring handles

A FileSystemFileHandle is a serializable object [FS], so one addressing a COS entry can be passed to another environment settings object, for example with postMessage(message, options). That is a second way to come by such a handle, alongside requestFileHandle(), so it is constrained here as well.

The serialization steps for a FileSystemFileHandle addressing a COS entry, given value and serialized, additionally:

  1. Set serialized.[[COSOrigin]] to value’s COS origin.

  2. Set serialized.[[COSMayRead]] to value’s may read.

The deserialization steps, given serialized, value and realm, additionally:

  1. If serialized.[[COSOrigin]] is not same origin with realm’s environment settings object’s origin, then throw a "DataCloneError" DOMException.

  2. Set value’s COS origin to serialized.[[COSOrigin]].

  3. Set value’s may read to serialized.[[COSMayRead]].

Note: Without the same origin check, transferring would defeat every disclosure control in this specification at once. A handle whose may read is true has already cleared apply availability gating for the origin that requested it; posting it to another origin would hand that origin the entry’s bytes without origins, PHL membership or GREASE’ing ever being evaluated for it. This matches the File System Standard’s existing treatment of handles, which are likewise only useful to a same origin recipient.

Note: may read travels with the handle rather than being recomputed, so a create-request handle that has not been written through stays unreadable after a transfer, and a read-request handle stays readable without being re-gated. Re-gating on arrival would be worse, not better: it would let a caller probe availability repeatedly by transferring the same handle.

5. Storage management

5.1. Storage limits

A user agent must impose a limit on the total number of bytes an origin may contribute to the COS registry through successful writes, to prevent a single origin from flooding the cache in an attempt to evict other origins' entries. The specific limit is implementation-defined. If an origin’s write would exceed its limit, the user agent must reject the write with a "QuotaExceededError" DOMException and should log a warning to the console.

Note: Because entries are content-addressable, an origin that repeatedly writes the same bytes under the same hash does not consume additional quota beyond the first successful write; see § 2.2 COS entries.

A user agent also has an implementation-defined maximum origins list length, a positive integer bounding how many origins a single origins list may contain. This is enforced both when a list is first supplied (see validate a COS request) and when one is later merged into an existing entry (see upgrade resource visibility), so that neither a single call nor the cumulative effect of many calls over time can grow an entry’s origins without bound. Beyond bounding memory use, this also keeps a list of origins from being usable as an undeclared substitute for "*"; see § 7.2 Cross-site probing.

Exceeding this limit is handled differently in each of those two places, because the two calls happen at very different points in an operation:

5.2. Eviction

This section is non-normative.

Under storage pressure, a user agent may evict entries from the COS registry, for example using a least-recently-used policy across the storing origins that have most recently accessed each entry. A user agent is expected to provide settings UI through which a user can inspect which files are stored, which origins have accessed each file, and manually delete entries or clear all Cross-Origin Storage data.

When a user clears an origin’s site data, the user agent should remove that origin from every storing origins set it appears in. If, after that removal, a COS entry’s storing origins is empty, the user agent may consider that entry for deletion.

5.3. Manually added entries

This section is non-normative.

The settings UI described above may let a user directly add a file they already have on disk to Cross-Origin Storage — for example, an AI model they downloaded independently of any website —​without any script ever calling requestFileHandle(). Such an entry has no requesting origin to attribute the write to, which raises two questions the imperative API does not answer by itself:

Once added this way, an entry with state "written" is otherwise indistinguishable from one a website wrote via requestFileHandle(): the same availability gating and visibility upgrade rules apply to it, exactly as they would for any other entry with the same origins value — including the on the PHL check, if the entry ends up "*"-scoped, which is the default for a manual add but not the only possible outcome.

5.4. Provenance metadata

This section is non-normative.

A COS entry deliberately records nothing about where its bytes came from. Identity is the hash alone: the same bytes are the same entry regardless of how many origins stored them or how many URLs they were fetched from (see § 2.2 COS entries). A URL is a property of one writer’s fetch rather than of the entry, so there is no non-arbitrary way to pick one for an entry several origins wrote; it is not verifiable the way the bytes are (see § 7.1 Resource integrity); and it carries considerably more about a user’s browsing than the coarse origin values in storing origins do, since a path or query string can name a specific document, account, or session. Recording it on the entry would therefore route a high-entropy signal around the disclosure limits that origins and § 7.3 Availability gating exist to impose.

The motivation for wanting it is nonetheless real: a user looking at a multi-gigabyte file in the settings UI described in § 5.2 Eviction, and a developer debugging why a requestFileHandle() call missed, would both like to know where a file came from. A user agent may serve that by keeping an implementation-private provenance record alongside an entry — for example, the URL each of its storing origins fetched the bytes from, and when — provided the record is treated as user agent state rather than as part of the entry:

Because such a record is invisible to content, whether a user agent keeps one, and how much it keeps, is left entirely to implementations. Nothing in this specification depends on it, and its presence or absence is not detectable by a site.

6. Permissions Policy integration

This specification defines a policy-controlled feature [permissions-policy] identified by the string "cross-origin-storage". Its default allowlist is self.

A Document that is not allowed to use this feature causes every requestFileHandle() call made from that Document (or from a worker whose owner is that Document) to reject with a "NotAllowedError" DOMException, before any hash validation, registry lookup, or write occurs.

7. Privacy and security considerations

This section is non-normative except where it uses RFC 2119 keywords; see also the security and privacy questionnaire in the repository.

7.1. Resource integrity

Every COS entry is keyed by, and its bytes are verified against, a cryptographic hash (see § 3.3 Creating and writing files). A site can therefore be sure that a file obtained through requestFileHandle() has exactly the same bytes it would have gotten by fetching hash itself. Developers cannot enumerate the contents of Cross-Origin Storage or access a file without already knowing its hash.

A COS hash is not a secret. It is a content identifier, trivially computable by anyone who already has the file, and is routinely public knowledge for the kind of widely distributed resources this specification targets — published, for example, alongside a model’s or a library’s release. Knowing a hash is therefore sufficient to look an entry up, but unlike a capability URL or a bearer token, it grants no access by itself: whether a lookup succeeds is governed entirely by origins and § 7.3 Availability gating, never by whether the hash happened to stay unpublished. Developers must not treat an unpublished hash as an access-control mechanism; an attacker interested in a specific file does not need to guess its hash, only to obtain the file itself (see § 7.2 Cross-site probing).

7.2. Cross-site probing

If a resource is only used by a narrow set of sites, an attacker able to learn that the resource is present in Cross-Origin Storage can infer that the user likely visited one of those sites. Because requestFileHandle() is the mechanism for learning presence, each call is, in effect, a probe. Two independent mechanisms in this specification bound what a probe can learn:

The first bullet only holds if a "chosen set of origins" stays meaningfully smaller than the web. Nothing about the shape of origins stops a caller from enumerating a very large number of origins — for example, a list assembled from a public top-sites ranking — which would functionally approximate global disclosure while bypassing the deliberate, explicit opt-in that "*" alone requires. The maximum origins list length (see § 5.1 Storage limits) bounds this: a limit small enough to fit genuine multi-property use cases (a handful of related origins under common control) but far short of any meaningful approximation of "every origin" keeps the restricted-origins form of origins from being usable as an undeclared substitute for "*".

A user agent should additionally rate-limit or otherwise throttle repeated requestFileHandle() calls from a single origin, and may apply on-device heuristics (for example, detecting a pattern of requests for hashes that appear to be generated per-user) to identify and block probing attempts, independent of whether the probed hashes are on the PHL.

A lookup performed by one of the host integrations in § 8 Integration with other specifications is equally a probe, even though it surfaces no error to the page: a site can observe whether its own server received the fallback request, which carries the same single bit that a "not found" outcome carries here. Such a lookup discloses nothing this specification’s gates would not, and those gates apply to it unchanged. A rate limit, however, is only effective if lookups from those surfaces are counted alongside requestFileHandle() calls, since a scriptable integration can issue them in a loop just as readily.

7.3. Availability gating

Whether a requestFileHandle() read from an origin outside storing origins succeeds always depends on whether the requesting origin is allowed to read the entry, governed by origins. For a "*"-scoped entry, a second, independent question also applies: whether the user agent is willing to disclose that the entry exists at all, governed by PHL membership and GREASE’ing. Both must hold for such an entry; for a list- or same-site-scoped entry, only the first question applies, though GREASE’ing may still suppress a disclosure that origins would otherwise permit. See § 3.2 Reading files for the normative algorithm. A "NotFoundError" is therefore not proof that a resource is absent: it may mean the resource does not exist, that the requesting origin is out of scope, that a "*"-scoped resource’s hash is not (yet, or ever) on the PHL, or that GREASE’ing suppressed a true positive. Developers must not treat "NotFoundError" as anything more specific than "fall back to the network."

Note: A "*"-scoped entry’s disclosure is additionally limited to requesting origins that can set or read third-party cookies in the current context; such an entry is not disclosed to an origin third-party cookies are unavailable to, regardless of PHL membership. This closes a gap PHL membership alone cannot: an attacker can build a cross-site identifier out of otherwise-ordinary, individually PHL-eligible resources by choosing which subset to write, and third-party cookie availability is the deciding factor in whether the resulting probe is any more informative than an ordinary tracking cookie would be.

7.4. GREASE’ing

A user agent may apply GREASE’ing (Generate Random Extensions And Sustain Extensibility): occasionally responding as if a disclosable entry were absent, even though § 7.3 Availability gating would otherwise permit disclosure. This adds noise that makes it harder for a site to distinguish a true absence from a privacy-motivated false negative, similar to the technique applied by UA Client Hints.

A user agent applying GREASE’ing must do so with judgement proportionate to the size of the entry in question. For small entries, where falling back to a network fetch is inexpensive, an occasional false negative is a reasonable privacy trade-off. A user agent must not apply GREASE’ing to entries whose size makes a spurious re-download clearly disproportionate to the resulting privacy benefit —​for example, gigabyte-scale AI model weights — because a false negative there forces a full, observable, and costly re-download rather than a cheap one.

7.5. Fingerprinting

The information an attacker can extract from probing Cross-Origin Storage is bounded by how popular the probed resource is. Learning that a user has a very popular resource, such as a common AI model or a widely used JavaScript library, reveals only that the user visited one of the (possibly very many) sites that use it. Learning that a user has a rare or unique resource is far more informative. A user agent is expected to apply on-device heuristics to detect resources whose hashes look deliberately unique per user (for example, a hash that has only ever been written by a single site, or that is requested with unusual frequency) and to treat such patterns as probing attempts, independent of nominal PHL status.

8. Integration with other specifications

This section is non-normative.

The COS registry defined here is also reachable from four host integrations without going through requestFileHandle() directly. Each integration is defined in its own host specification, not in this document; this specification defines only the shared underlying concepts (COS hash, COS entry, availability gating, and the Public Hash List) that those integrations are expected to build on.

For illustration only, here is the same globally-shared resource opted into Cross-Origin Storage through each of the four forms:

<script src="popular-library.js" integrity="sha256-abc123..." crossoriginstorage="*"></script>
import data from "popular-resource.ext" with {
  integrity: "sha256-abc123...",
  crossOriginStorage: "*",
};
@font-face {
  font-family: "Popular Font";
  src: url("popular-font.woff2" integrity("sha256-abc123...") cross-origin-storage(*));
}
const response = await fetch("popular-resource.ext", {
  integrity: "sha256-abc123...",
  crossOriginStorage: "*",
});

These snippets are illustrative only. None of this syntax is defined by this specification, and the authoritative grammar for each form — including how a restricted, non-"*" origins value is spelled — belongs to its own host specification, not to this document. The spellings deliberately differ: a RequestInit member can take a sequence<DOMString>, matching origins exactly, while an attribute value, an import attribute value, and a CSS modifier argument can only carry text.

All four forms are expected to share the processing model of first consulting the COS registry for a matching, disclosable entry before falling back to a network fetch, and of storing a successfully fetched and integrity-verified resource into the COS registry for reuse by other origins, exactly as requestFileHandle() does. Discussion of these integrations belongs in the tracker of the host specification that would carry them, not in this repository’s issue tracker.

Acknowledgments

Many thanks for valuable feedback from Tab Atkins-Bittner, Yash Raj Bharti, and Joshua Lochner, and for valuable inspiration or ideas from Kenji Baheux and Kevin Moore.

This specification includes material modeled on File System, which is available under the W3C Software and Document License.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[FileAPI]
Marijn Kruisselbrink. File API. URL: https://w3c.github.io/FileAPI/
[FS]
Austin Sullivan. File System Standard. Living Standard. URL: https://fs.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PERMISSIONS-POLICY]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBCRYPTO]
Daniel Huigens. Web Cryptography Level 2. URL: https://w3c.github.io/webcrypto/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Non-Normative References

[SRI]
Frederik Braun. Subresource Integrity. URL: https://w3c.github.io/webappsec-subresource-integrity/
[STREAMS]
Adam Rice; et al. Streams Standard. Living Standard. URL: https://streams.spec.whatwg.org/
[UA-CLIENT-HINTS]
User-Agent Client Hints. Draft Community Group Report. URL: https://wicg.github.io/ua-client-hints/

IDL Index

[Exposed=(Window,Worker), SecureContext]
interface CrossOriginStorageManager {
  Promise<FileSystemFileHandle> requestFileHandle(
      CrossOriginStorageRequestFileHandleHash hash,
      optional CrossOriginStorageRequestFileHandleOptions options = {});
};

dictionary CrossOriginStorageRequestFileHandleHash {
  required DOMString value;
  required DOMString algorithm;
};

dictionary CrossOriginStorageRequestFileHandleOptions {
  boolean create = false;
  (DOMString or sequence<DOMString>) origins;
};

interface mixin NavigatorCrossOriginStorage {
  [SameObject, SecureContext] readonly attribute CrossOriginStorageManager crossOriginStorage;
};
Navigator includes NavigatorCrossOriginStorage;
WorkerNavigator includes NavigatorCrossOriginStorage;

Issues Index

The retrieval protocol, update cadence, data format, popularity thresholds, and governance of the Public Hash List are designed in detail as a companion artifact to this specification, in the spirit of how the [URL] Standard’s public suffix list is a companion data file rather than normative prose — see the Public Hash List explainer in this repository. That design proposes governance by the WHATWG, modeled on the Public Suffix List’s cross-vendor, rolling-release precedent, and admission criteria keyed on independently corroborated ubiquity —​a concrete instance of the k-anonymity-style bar described above, applied once, offline, as part of how a hash is admitted, not as a check the user agent repeats per query. None of this is yet established outside this proposal. An early, non-normative code prototype of the list itself is maintained in this repository, at public-hash-list/implementation/ — a pragmatic interim home; a dedicated, cross-vendor repository remains the governance target described in the PHL explainer. This specification only depends on whether a hash is on the PHL, not on any particular retrieval mechanism or governance model, so it remains correct regardless of how that design evolves.
The File System Standard [FS] does not yet define an extension point for a dependent specification to hook additional per-write validation into FileSystemWritableFileStream’s closing steps. Until such a hook exists, this section describes the required behavior directly; a future revision is expected to formally integrate with [FS].