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 with declarative markup — an HTML crossoriginstorage attribute on link and script, a crossOriginStorage JavaScript import attribute, and a CSS cross-origin-storage() <request-url-modifier> — each defined in its host language’s own 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.

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 available at tomayac/public-hash-list. 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.

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. 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.

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. Let handle be the result of creating a new FileSystemFileHandle [FS] whose locator addresses entry within the Cross-Origin Storage file system, in realm.

  5. Set handle’s requested origins to requestedOrigins.

    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.

  6. 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, leaving entry unmodified.

    2. 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. Run upgrade resource visibility given entry and the handle’s requested origins.

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.

A FileSystemFileHandle addressing a COS entry entry can exist while entry’s state is still "pending" — for example, right after a create request, before the corresponding write has completed. Calling getFile() on such a handle must reject with a "NotAllowedError" DOMException, for the same reason a concurrent requestFileHandle() call for the same hash does (see § 3.2 Reading files): even the caller that requested the handle must not be able to observe a not-yet-verified placeholder as if it were the file’s real contents.

Once entry’s state is "written", getFile() called on such a handle returns a File whose contents are entry’s bytes; this is exactly the behavior [FS] already defines for a file entry whose binary data is entry’s bytes.

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.

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.

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."

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 declaratively, without going through requestFileHandle() directly, from three host languages. Each integration is defined in the specification for its own host language, 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 three 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(*));
}

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-language specification, not to this document.

All three 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-language 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/
[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 available at tomayac/public-hash-list. 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].