├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .pr-preview.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── Makefile
├── README.md
├── select-url.md
├── shared-storage-tester-list.md
├── spec.bs
└── w3c.json
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | pull_request:
4 | branches: [main]
5 | paths: ["**.bs"]
6 | push:
7 | branches: [main]
8 | paths: ["**.bs"]
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: w3c/spec-prod@v2
16 | with:
17 | TOOLCHAIN: bikeshed
18 | DESTINATION: index.html
19 | SOURCE: spec.bs
20 | GH_PAGES_BRANCH: gh-pages
21 | BUILD_FAIL_ON: warning
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | spec.html
2 | spec-remote.html
3 |
--------------------------------------------------------------------------------
/.pr-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_file": "spec.bs",
3 | "type": "bikeshed"
4 | }
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/).
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Web Platform Incubator Community Group
2 |
3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License
4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions,
5 | you must join the CG.
6 |
7 | If you are not the sole contributor to a contribution (pull request), please identify all
8 | contributors in the pull request comment.
9 |
10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
11 |
12 | ```
13 | +@github_username
14 | ```
15 |
16 | If you added a contributor by mistake, you can remove them in a comment with:
17 |
18 | ```
19 | -@github_username
20 | ```
21 |
22 | If you are making a pull request on behalf of someone else but you had no part in designing the
23 | feature, you can remove yourself with the above syntax.
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors
2 | under the
3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
4 |
5 | Contributions to Specifications are made under the
6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
7 |
8 | Contributions to Test Suites are made under the
9 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html)
10 |
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL = /bin/bash
2 |
3 | # Automatically delete any target whose recipe fails.
4 | .DELETE_ON_ERROR:
5 |
6 | spec.html: spec.bs
7 | bikeshed --die-on=warning spec $< $@
8 |
9 | spec-remote.html: spec.bs
10 | trap 'echo; cat $@; echo;' ERR; \
11 | curl >$@ https://api.csswg.org/bikeshed/ \
12 | --no-progress-meter \
13 | --fail-with-body \
14 | --form die-on=warning \
15 | --form file=@$<
16 |
17 | # These targets do not correspond to files.
18 | .PHONY: local remote clean
19 |
20 | local: spec.html
21 |
22 | remote: spec-remote.html
23 | cp $< spec.html
24 |
25 | clean:
26 | rm -f spec.html spec-remote.html
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shared Storage API Explainer
2 |
3 | Authors: Alex Turner, Camillia Smith Barnes, Josh Karlin, Yao Xiao
4 |
5 |
6 | ## Introduction
7 |
8 | In order to prevent cross-site user tracking, browsers are [partitioning](https://blog.chromium.org/2020/01/building-more-private-web-path-towards.html) all forms of storage (cookies, localStorage, caches, etc) by top-frame site. But, there are many legitimate use cases currently relying on unpartitioned storage that will vanish without the help of new web APIs. We’ve seen a number of APIs proposed to fill in these gaps (e.g., [Conversion Measurement API](https://github.com/WICG/conversion-measurement-api), [Private Click Measurement](https://github.com/privacycg/private-click-measurement), [Storage Access](https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API), [Private State Tokens](https://github.com/WICG/trust-token-api), [TURTLEDOVE](https://github.com/WICG/turtledove), [FLoC](https://github.com/WICG/floc)) and some remain (including cross-origin A/B experiments and user measurement). We propose a general-purpose storage API that can help to serve as common infrastructure for privacy preserving cross-site use cases.
9 |
10 | Shared Storage is a key/value store that is partitioned by calling origin (but not top-frame site). The keys and values are strings. While it's possible to write to Shared Storage from nearly anywhere (including response headers!), it is only possible to read from Shared Storage in tightly controlled environments, such as a JavaScript worklet environment which is provided by Shared Storage. These worklets have no capability of communicating with the outside world. They have no network communication and no `postMessage`. The only way data can leave these worklets, is via privacy-preserving APIs.
11 |
12 | ### Specification
13 |
14 | See the [draft specification](https://wicg.github.io/shared-storage/).
15 |
16 | ## APIs built on top of Shared Storage
17 |
18 | This document only describes the core shared storage framework and infrastructure to store cross-site data privately and to read that data from within a secure worklet environment. APIs that use shared storage's data to produce some output are linked to below.
19 |
20 | ### Private Aggregation
21 |
22 | The [Private Aggregation](https://github.com/patcg-individual-drafts/private-aggregation-api) API allows for aggregated histograms to be sent based on data read from shared storage. The histograms are differentially private.
23 |
24 |
25 | ### Select URL
26 |
27 | The [selectURL](https://github.com/WICG/shared-storage/blob/main/select-url.md) API allows for content selection based on cross-site data. It takes 8 possible URLs as input and sends them to a worklet which selects from a small list of URLs. The chosen URL is stored in a fenced frame config as an opaque form that can only be read by a [fenced frame](https://github.com/WICG/fenced-frame); the embedder does not learn this information.
28 |
29 |
30 | ## Demonstration
31 |
32 | You can [try out](https://shared-storage-demo.web.app/) Shared Storage along with some APIs built for it using Chrome 104+.
33 |
34 |
35 | ### Example 1: Writing an experiment id to Shared Storage from a document
36 |
37 | Since Shared Storage is meant for writing from anywhere, but reading is tightly constrained, it's not actually possible to know what you might have written to your storage from other sites. Is this the first time you've seen this user? Who knows! As such, `Shared Storage` provides some useful functions beyond just `set` to write to keys only if they're not already present, and to append to a value rather than overwrite it.
38 |
39 | For example, let's say that you wanted to add a user to an experiment group, with a random assignment. But you want that group assignment to be sticky for the user across all of the sites that they visit that your third-party script is on. You may not know if you've ever written this key from this site before, but you certainly don't know if you've set it from another site. To solve this issue, utilize the `ignoreIfPresent` option.
40 |
41 | ```js
42 | try {
43 | sharedStorage.set('group', Math.floor(Math.random() * 1000), { ignoreIfPresent: true });
44 | } catch (error) {
45 | // Error handling
46 | }
47 | ```
48 | And `Shared Storage` will only write the value if the key is not already present.
49 |
50 | ### Example 2: Writing to Shared Storage via a worklet
51 |
52 | In the event that `ignoreIfPresent` is not sufficient, and you need to read your existing `Shared Storage` data before adding new data, consider passing the information that you want to record to a worklet, and letting the worklet read the existing data and perform the write. Like so:
53 |
54 | ```js
55 | try {
56 | const worklet = sharedStorage.createWorklet('https://site.example/writerWorklet.js');
57 | worklet.run('write', {data: {group: Math.floor(Math.random() * 1000)}});
58 | } catch (error) {
59 | // Error handling
60 | }
61 | ```
62 |
63 | And your `writerWorklet.js` script would look like this:
64 | `writerWorklet.js`
65 | ```js
66 | class Writer {
67 | async run(data) {
68 | const existingGroup = sharedStorage.get('group');
69 | if (!existingGroup) {
70 | cibst newGroup = data['group'];
71 | sharedStorage.set('group', newGroup);
72 | }
73 | }
74 | }
75 | register('write', Writer);
76 | ```
77 |
78 | ### Example 3: Writing to Shared Storage with response headers
79 | It may be faster and more convenient to write to Shared Storage directly from response headers than from JavaScript. This is encouraged in cases where data is coming from a server anyway as it's faster and less intensive than JavaScript methods if you're writing to an origin other than the current document's origin.
80 |
81 | Response headers can be used on document, image, and fetch requests.
82 |
83 | e.g.,:
84 | ```html
85 |
86 | ```
87 |
88 | The document request for "https://site.example/iframe" will include a `Sec-Shared-Storage-Writable: ?1` request header. Any request with this header can have a corresponding `Shared-Storage-Write` response header that can write, like so:
89 | ```js
90 | Shared-Storage-Write: set;key="group";value="4";ignore_if_present
91 | ```
92 |
93 | ### Example 4: Counting the number of views your content has received across sites
94 | To count the number of times the user has viewed your third-party content, consider using the append option. Like so:
95 |
96 | e.g.,:
97 | ```js
98 | try {
99 | window.sharedStorage.append('count', '1');
100 | } catch (error) {
101 | // Error handling
102 | }
103 | ```
104 |
105 | Then, sometime later in your worklet, you can get the total count:
106 | ```js
107 | class Counter {
108 | async run(data) {
109 | const countLog = data['count']; // e.g.,: '111111111'
110 | const count = countLog.length;
111 | // do something useful with this data (such as recording an aggregate histogram) here...
112 | }
113 | }
114 | register('count', Counter);
115 | ```
116 |
117 | ## Goals
118 |
119 | This API intends to support the storage and access needs for a wide array of cross-site data use cases. This prevents each API from having to create its own bespoke storage APIs.
120 |
121 |
122 | ## Related work
123 |
124 | There have been multiple privacy proposals ([SPURFOWL](https://github.com/AdRoll/privacy/blob/main/SPURFOWL.md), [SWAN](https://github.com/1plusX/swan), [Aggregated Reporting](https://github.com/csharrison/aggregate-reporting-api)) that have a notion of write-only storage with limited output. Shared Storage allows for each of those use cases, with only one storage API which is easier for developers to learn and requires less browser code. We’d also like to acknowledge the [KV Storage](https://github.com/WICG/kv-storage) explainer, to which we turned for API-shape inspiration.
125 |
126 |
127 | ## Proposed API surface
128 |
129 |
130 | ### Outside of worklets (e.g., places where writing can happen)
131 | The modifier methods (`set`, `append`, `delete`, `clear`, and `batchUpdate`) should be made generally available across most any context. That includes top-level documents, iframes, shared storage worklets, Protected Audience worklets, service workers, dedicated workers, etc.
132 |
133 | The shared storage worklet invocation methods (`addModule`, `createWorklet`, and `run`) are available within document contexts.
134 |
135 |
136 | * `window.sharedStorage.set(key, value, options)`
137 | * Sets `key`’s entry to `value`.
138 | * `key` and `value` are both strings.
139 | * Options include:
140 | * `ignoreIfPresent` (defaults to false): if true, a `key`’s entry is not updated if the `key` already exists. The embedder is not notified which occurred.
141 | * `withLock`: acquire a lock on the designated resource before executing. See [Locking for modifier methods](#locking-for-modifier-methods) for details.
142 | * `window.sharedStorage.append(key, value, options)`
143 | * Appends `value` to the entry for `key`. Equivalent to `set` if the `key` is not present.
144 | * Options include:
145 | * `withLock`: acquire a lock on the designated resource before executing. See [Locking for modifier methods](#locking-for-modifier-methods) for details.
146 | * `window.sharedStorage.delete(key, options)`
147 | * Deletes the entry at the given `key`.
148 | * Options include:
149 | * `withLock`: acquire a lock on the designated resource before executing. See [Locking for modifier methods](#locking-for-modifier-methods) for details.
150 | * `window.sharedStorage.clear(options)`
151 | * Deletes all entries.
152 | * Options include:
153 | * `withLock`: acquire a lock on the designated resource before executing. See [Locking for modifier methods](#locking-for-modifier-methods) for details.
154 | * `window.sharedStorage.batchUpdate(methods, options)`
155 | * Execute `methods` in order. All updates within a `batchUpdate()` call are executed as a single unit of work. If any method fails, none of them will commit.
156 | * `methods` is an array of method objects defining the operations to perform. Each object must be one of the following types: `SharedStorageSetMethod`, `SharedStorageAppendMethod`, `SharedStorageDeleteMethod`, or `SharedStorageClearMethod`. Each method object's constructor accepts the same parameters as the corresponding individual method (e.g., `set`, `append`, `delete`, `clear`). For transactional integrity, the `withLock` option within these methods is not permitted and will result in an error, but the lock for the `batchUpdate()` method itself (if specified) will be acquired.
157 | * Options include:
158 | * `withLock`: acquire a lock on the designated resource before executing. See [Locking for modifier methods](#locking-for-modifier-methods) for details.
159 | * This method, with the `withLock` option, allows multiple modifier methods to be executed atomically and mutually exclusively with other concurrent operations acquiring the same lock, enabling use cases where a website needs to maintain consistency while updating data organized across multiple keys.
160 | * `window.sharedStorage.worklet.addModule(url, options)`
161 | * Loads and adds the module to the worklet (i.e. for registering operations). The handling should follow the [worklet standard](https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule), unless clarified otherwise below.
162 | * This method can only be invoked once per worklet. This is because after the initial script loading, shared storage data (for the invoking origin) will be made accessible inside the worklet environment, which can be leaked via subsequent `addModule()` (e.g. via timing).
163 | * `url`'s origin need not match that of the context that invoked `addModule(url)`.
164 | * If `url` is cross-origin to the invoking context, the worklet will use the invoking context's origin as its partition origin for accessing shared storage data and for budget checking and withdrawing.
165 | * Also, for a cross-origin`url`, the CORS protocol applies.
166 | * Redirects are not allowed.
167 | * `window.sharedStorage.worklet.run(name, options)`
168 | * Runs the operation previously registered by `register()` with matching `name`. Does nothing if there’s no matching operation.
169 | * Returns a promise that resolves to `undefined` when the operation is queued:
170 | * Options can include:
171 | * `data`, an arbitrary serializable object passed to the worklet.
172 | * `keepAlive` (defaults to false), a boolean denoting whether the worklet should be retained after it completes work for this call.
173 | * If `keepAlive` is false or not specified, the worklet will shutdown as soon as the operation finishes and subsequent calls to it will fail.
174 | * To keep the worklet alive throughout multiple calls to `run()`, each of those calls must include `keepAlive: true` in the `options` dictionary.
175 | * `window.sharedStorage.run(name, options)`
176 | * The behavior is identical to `window.sharedStorage.worklet.run(name, options)`.
177 | * `window.sharedStorage.createWorklet(url, options)`
178 | * Creates a new worklet, and loads and adds the module to the worklet (similar to the handling for `window.sharedStorage.worklet.addModule(url, options)`).
179 | * By default, the worklet uses the invoking context's origin as its partition origin for accessing shared storage data and for budget checking and withdrawing.
180 | * To instead use the worklet script origin (i.e. `url`'s origin) as the partition origin for accessing shared storage, pass the `dataOrigin` option with "script-origin" as its value in the `options` dictionary.
181 | * To use a custom origin as the partition origin for accessing shared storage, pass the `dataOrigin` option with the serialized partition origin (i.e. the [URL serialization](https://url.spec.whatwg.org/#url-serializing) of a URL with the same scheme, host, and port as the partition origin, but whose other components are empty) as its value in the `options` dictionary.
182 | * Supported values for the `dataOrigin` option, if used, are the keywords "script-origin" and "context-origin", as well as any valid serialized HTTPS origin, e.g. "https://custom-data-origin.example".
183 | * "script-origin" designates the worklet script origin as the data partition origin.
184 | * "context-origin" (default) designates the invoking context origin as the data partition origin.
185 | * A serialized HTTPS origin designates itself as the data partition origin.
186 | * When a valid serialized HTTPS URL is passed as the value for `dataOrigin` and the parsed URL's origin is cross-origin to both the invoking context's origin and the worklet script's origin, the parsed URL's origin must host a JSON file at the /.well-known/ path "/.well-known/shared-storage/trusted-origins" with an array of dictionaries, each with keys `scriptOrigin` and `contextOrigin`. The values for these keys should be either a string or an array of strings.
187 | * A string value should be either a serialized origin or `"*"`, where `"*"` matches all origins.
188 | * A value that is an array of strings should be a list of serialized origins.
189 | * For example, the following JSON at "https://custom-data-origin.example/.well-known/shared-storage/trusted-origins" allows script from "https://script-origin.a.example" to process "https://custom-data-origin.example"'s shared storage data when `createWorklet` is invoked in the context "https://context-origin.a.example", it allows script from "https://script-origin.b.example" to process "https://custom-data-origin.example"'s shared storage data when `createWorklet` is invoked in the context of either "https://context-origin.a.example" or "https://context-origin.b.example", and it also allows script from "https://script-origin.c.example" and "https://script-origin.d.example" to process "https://custom-data-origin.example"'s shared storage data when `createWorklet` is invoked in any origin's context.
190 | ```
191 | [
192 | {
193 | scriptOrigin: "https://script-origin.a.example",
194 | contextOrigin: "https://context-origin.a.example"
195 | },
196 | {
197 | scriptOrigin: "https://script-origin.b.example",
198 | contextOrigin: ["https://context-origin.a.example", "https://context-origin.b.example"]
199 | },
200 | {
201 | scriptOrigin: ["https://script-origin.c.example", "https://script-origin.d.example"],
202 | contextOrigin: "*"
203 | }
204 | ]
205 | ```
206 | * The object that the returned Promise resolves to has the same type with the implicitly constructed `window.sharedStorage.worklet`. However, for a worklet created via `window.sharedStorage.createWorklet(url, options)`, only `selectURL()` and `run()` are available, whereas calling `addModule()` will throw an error. This is to prevent leaking shared storage data via `addModule()`, similar to the reason why `addModule()` can only be invoked once on the implicitly constructed `window.sharedStorage.worklet`.
207 | * Redirects are not allowed.
208 | * When the module script's URL's origin is cross-origin with the worklet's creator window's origin and when `dataOrigin` is "script-origin" (or when `dataOrigin` is a valid serialized HTTPS URL that is same-origin to the worklet's script's origin), the check for trusted origins at the [/.well-known/ path](#well-known) will be skipped, and a `Shared-Storage-Cross-Origin-Worklet-Allowed: ?1` response header is required instead.
209 | * The script server must carefully consider the security risks of allowing worklet creation by other origins (via `Shared-Storage-Cross-Origin-Worklet-Allowed: ?1` and CORS), because this will also allow the worklet creator to run subsequent operations, and a malicious actor could poison and use up the worklet origin's budget.
210 | * Note that for the script server's information, the request header "Sec-Shared-Storage-Data-Origin" will be included with the value of the serialized data partition origin to be used if the data partition origin is cross-origin to the invoking context's origin.
211 |
212 |
213 |
214 | ### In the worklet, during `sharedStorage.worklet.addModule(url, options)` or `sharedStorage.createWorklet(url, options)`
215 | * `register(name, operation)`
216 | * Registers a shared storage worklet operation with the provided `name`.
217 | * `operation` should be a class with an async `run()` method.
218 | * For the operation to work with `sharedStorage.run()`, `run()` should take `data` as an argument and return nothing. Any return value is [ignored](#default).
219 |
220 |
221 | ### In the worklet, during an operation
222 | * `sharedStorage.get(key)`
223 | * Returns a promise that resolves into the `key`‘s entry or an empty string if the `key` is not present.
224 | * `sharedStorage.length()`
225 | * Returns a promise that resolves into the number of keys.
226 | * `sharedStorage.keys()` and `sharedStorage.entries()`
227 | * Returns an async iterator for all the stored keys or [key, value] pairs, sorted in the underlying key order.
228 | * `sharedStorage.set(key, value, options)`, `sharedStorage.append(key, value, options)`, `sharedStorage.delete(key, options)`, `sharedStorage.clear(options)`, and `sharedStorage.batchUpdate(methods, options)`
229 | * Same as outside the worklet, except that the promise returned only resolves into `undefined` when the operation has completed.
230 | * `sharedStorage.context`
231 | * From inside a worklet created inside a [fenced frame](https://github.com/wicg/fenced-frame/), returns a string of contextual information, if any, that the embedder had written to the [fenced frame](https://github.com/wicg/fenced-frame/)'s [FencedFrameConfig](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md) before the [fenced frame](https://github.com/wicg/fenced-frame/)'s navigation.
232 | * If no contextual information string had been written for the given frame, returns undefined.
233 | * `interestGroups()`
234 | * Returns a promise that resolves into an array of `StorageInterestGroup`. A `StorageInterestGroup` is a dictionary that extends the [AuctionAdInterestGroup](https://wicg.github.io/turtledove/#dictdef-auctionadinterestgroup) dictionary with the following attributes:
235 | * unsigned long long `joinCount`
236 | * unsigned long long `bidCount`
237 | * sequence<[PreviousWin](https://wicg.github.io/turtledove/#typedefdef-previouswin)> `prevWinsMs`
238 | * USVString `joiningOrigin`
239 | * long long `timeSinceGroupJoinedMs`
240 | * long long `lifetimeRemainingMs`
241 | * long long `timeSinceLastUpdateMs`
242 | * long long `timeUntilNextUpdateMs`
243 | * unsigned long long `estimatedSize`
244 | * The approximate size of the contents of this interest group, in bytes.
245 | * The [AuctionAdInterestGroup](https://wicg.github.io/turtledove/#dictdef-auctionadinterestgroup)'s [lifetimeMs](https://wicg.github.io/turtledove/#dom-auctionadinterestgroup-lifetimems) field will remain unset. It's no longer applicable at query time and is replaced with attributes `timeSinceGroupJoinedMs` and `lifetimeRemainingMs`.
246 | * This API provides the Protected Audience buyer with a better picture of what's happening with their users, allowing for Private Aggregation reports.
247 | * `navigator.locks.request(resource, callback)` and `navigator.locks.request(resource, options, callback)`
248 | * Acquires a lock on `resource` and invokes `callback` with the lock held. `navigator.locks` returns a `LockManager` as it does in a `Window`. See the [request](https://w3c.github.io/web-locks/#dom-lockmanager-request) method in Web Locks API for details.
249 | * Lock Scope: shared storage locks are partitioned by the shared storage data origin, and are independent of any locks obtained via `navigator.locks.request` in a `Window` or `Worker` context. This prevents contention between shared storage locks and other locks, ensuring that shared storage data cannot be inadvertently leaked.
250 | * Functions exposed by APIs built on top of Shared Storage such as the [Private Aggregation API](https://github.com/alexmturner/private-aggregation-api), e.g. `privateAggregation.contributeToHistogram()`.
251 | * These functions construct and then send an aggregatable report for the private, secure [aggregation service](https://github.com/WICG/conversion-measurement-api/blob/main/AGGREGATION_SERVICE_TEE.md).
252 | * The report contents (e.g. key, value) are encrypted and sent after a delay. The report can only be read by the service and processed into aggregate statistics.
253 | * After a Shared Storage operation has been running for 5 seconds, Private Aggregation contributions are timed out. Any future contributions are ignored and contributions already made are sent in a report as if the Shared Storage operation had completed.
254 |
255 |
256 | ### From response headers
257 |
258 | * `batchUpdate()` can be triggered via the HTTP response header `Shared-Storage-Write`.
259 | * This may provide a large performance improvement over creating a cross-origin iframe and writing from there, if a network request is otherwise required.
260 | * `Shared-Storage-Write` is a [List Structured Header](https://www.rfc-editor.org/rfc/rfc8941.html#name-lists).
261 | * Each member of the [List](https://www.rfc-editor.org/rfc/rfc8941.html#name-lists) is a [Token Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-tokens) denoting either 1) the individual modifier method (`set`, `append`, `delete`, `clear`), with any arguments for the method as associated [Parameters](https://www.rfc-editor.org/rfc/rfc8941.html#name-parameters), or 2) the options to apply to the whole batch, with any individual options as associated [Parameters](https://www.rfc-editor.org/rfc/rfc8941.html#name-parameters). A string type argument or option (`key`, `value`, `with_lock`) can take the form of a [Token Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-tokens) or a [String Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-strings) or a [Byte Sequence Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences). A boolean type option (`ignore_if_present`) can take the form of a [Boolean Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-booleans).
262 | * The modifier methods [Items](https://www.rfc-editor.org/rfc/rfc8941.html#name-items) in the [List](https://www.rfc-editor.org/rfc/rfc8941.html#name-lists) are handled in the order they appear.
263 | * If multiple `options` [Items](https://www.rfc-editor.org/rfc/rfc8941.html#name-items) appear in the [List](https://www.rfc-editor.org/rfc/rfc8941.html#name-lists), the last one will be used.
264 | * The individual modifier methods correspond to [Items](https://www.rfc-editor.org/rfc/rfc8941.html#name-items) as follows:
265 | * `set(, , {ignoreIfPresent: true})` ←→ `set;key=;value=;ignore_if_present`
266 | * `set(, , {ignoreIfPresent: false})` ←→ `set;key=;value=;ignore_if_present=?0`
267 | * `set(, )` ←→ `set;key=;value=`
268 | * `append(, )` ←→ `append;key=;value=`
269 | * `delete()` ←→ `delete;key=`
270 | * `clear()` ←→ `clear`
271 | * The `batchUpdate()` options corresponds to an [Item](https://www.rfc-editor.org/rfc/rfc8941.html#name-items) as follows:
272 | * `{withLock: }` ←→ `options;with_lock=`
273 | * Example 1: Single Update
274 | * Header value: `set;key="123";value="456";ignore_if_present`.
275 | * JavaScript equivalent: `sharedStorage.batchUpdate([new SharedStorageSetMethod("123", "456", {ignoreIfPresent: true})])`. Note that this is also equivalent to: `sharedStorage.set("123", "456", {ignoreIfPresent: true})`.
276 | * Example 2: Batch Update with Lock
277 | * Header value: `set;key="123";value="456";ignore_if_present, append;key=abc;value=def, options;with_lock="report-lock"`.
278 | * JavaScript equivalent: `sharedStorage.batchUpdate([new SharedStorageSetMethod("123", "456", {ignoreIfPresent: true}), new SharedStorageAppendMethod("abc", "def")], { withLock: "report-lock" })`.
279 | * `` and `` [Parameters](https://www.rfc-editor.org/rfc/rfc8941.html#name-parameters) are of type [String](https://www.rfc-editor.org/rfc/rfc8941.html#name-strings) or [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences).
280 | * Note that [Strings](https://www.rfc-editor.org/rfc/rfc8941.html#name-strings) are defined as zero or more [printable ASCII characters](https://www.rfc-editor.org/rfc/rfc20.html), and this excludes tabs, newlines, carriage returns, and so forth.
281 | * To pass a key and/or value that contains non-ASCII and/or non-printable [UTF-8](https://www.rfc-editor.org/rfc/rfc3629.html) characters, specify it as a [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences).
282 | * A [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences) is delimited with colons and encoded using [base64](https://www.rfc-editor.org/rfc/rfc4648.html).
283 | * The sequence of bytes obtained by decoding the [base64](https://www.rfc-editor.org/rfc/rfc4648.html) from the [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences) must be valid [UTF-8](https://www.rfc-editor.org/rfc/rfc3629.html).
284 | * For example:
285 | * `:aGVsbG8K:` encodes "hello\n" in a [UTF-8](https://www.rfc-editor.org/rfc/rfc3629.html) [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences) (where "\n" is the newline character).
286 | * `:8J+YgA==:` encodes "😀" in a [UTF-8](https://www.rfc-editor.org/rfc/rfc3629.html) [Byte Sequence](https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequences).
287 | * Remember that results returned via `get()` are [UTF-16](https://www.rfc-editor.org/rfc/rfc2781.html) [DOMStrings](https://webidl.spec.whatwg.org/#idl-DOMString).
288 | * Modifying shared storage via response headers requires a prior opt-in via a corresponding HTTP request header `Sec-Shared-Storage-Writable: ?1`.
289 | * The request header can be sent along with `fetch` requests via specifying an option: `fetch(, {sharedStorageWritable: true})`.
290 | * The request header can alternatively be sent on document or image requests either
291 | * via specifying a boolean content attribute, e.g.:
292 | * ``
293 | * `
`
294 | * or via an equivalent boolean IDL attribute, e.g.:
295 | * `iframe.sharedStorageWritable = true`
296 | * `img.sharedStorageWritable = true`.
297 | * Redirects will be followed, and the request header will be sent to the host server for the redirect URL.
298 | * The origin used for Shared Storage is that of the server that sends the `Shared-Storage-Write` response header(s).
299 | * If there are no redirects, this will be the origin of the request URL.
300 | * If there are redirects, the origin of the redirect URL that is accompanied by the `Shared-Storage-Write` response header(s) will be used.
301 | * The response header will only be honored if the corresponding request included the request header: `Sec-Shared-Storage-Writable: ?1`.
302 | * See example usage below.
303 |
304 | ### Locking for Modifier Methods
305 |
306 | All modifier methods (`set`, `append`, `delete`, `clear`, `batchUpdate`), whether invoked from JavaScript or from response headers, accept a `withLock: ` option. This option instructs the method to acquire a lock on the designated resource before executing.
307 |
308 | The locks requested this way are partitioned by the shared storage data origin, and are independent of any locks obtained via `navigator.locks.request` in a Window or Worker context. Note that they share the same scope with the locks obtained via `navigator.locks.request` in the SharedStorageWorklet context.
309 |
310 | Unlike `navigator.locks.request`, which offers various configuration options, the locks requested this way always use the default settings:
311 | * `mode: "exclusive"`: The lock is never shared with other locks.
312 | * `steal: false`: The lock will not preempt other locks.
313 | * `ifAvailable: false`: If the lock is currently held by others, keep waiting and don't skip.
314 |
315 | #### Example: Report on Multiple Keys
316 |
317 | This example uses a lock to ensure that the read and delete operations inside the worklet runs atomically, preventing interference from the write operations outside the worklet.
318 |
319 | Window context:
320 |
321 | ```js
322 | try {
323 | sharedStorage.batchUpdate([
324 | new SharedStorageSetMethod('key0', calculateValueFor('key0')),
325 | new SharedStorageSetMethod('key1', calculateValueFor('key1'))
326 | ], { withLock: 'report-lock' });
327 |
328 | await sharedStorage.worklet.addModule('report-on-multiple-keys-script.js');
329 | await sharedStorage.worklet.run('report-on-multiple-keys');
330 | } catch (error) {
331 | // Handle error.
332 | }
333 | ```
334 |
335 | In the worklet script (`report-on-multiple-keys-script.js`):
336 |
337 | ```js
338 | class ReportOnMultipleKeysOperation {
339 | async run(data) {
340 | await navigator.locks.request("report-lock", async (lock) => {
341 | const value1 = await sharedStorage.get('key1');
342 | const value2 = await sharedStorage.get('key2');
343 |
344 | // Record an aggregate histogram with `value1` and `value2` here...
345 |
346 | await sharedStorage.delete('key1');
347 | await sharedStorage.delete('key2');
348 | });
349 | }
350 | }
351 | register('report-on-multiple-keys', ReportOnMultipleKeysOperation);
352 | ```
353 |
354 | #### Caveat: Unexpected ordering
355 |
356 | Modifier methods may block due to the lock, so may not execute in the order they appear in the code.
357 |
358 | ```js
359 | // Resolve immediately. Internally, this may block to wait for the lock to be granted.
360 | sharedStorage.set('key0', 'value1', { withLock: 'resource0' });
361 |
362 | // Resolve immediately. Internally, this will execute immediately.
363 | sharedStorage.set('key0', 'value2');
364 | ```
365 |
366 | Developers should be mindful of this potential ordering issue.
367 |
368 | ### Recommendations for lock usage
369 |
370 | If only a single key is involved, and the data is accessed at most once within and outside worklet, then the lock is unnecessary. This is because each access is inherently atomic. Example: [A/B experiment](https://github.com/WICG/shared-storage/blob/main/select-url.md#simple-example-consistent-ab-experiments-across-sites).
371 |
372 | If the worklet performs both read and write on the same key, then the lock is likely necessary. Example: [creative selection by frequency](https://github.com/WICG/shared-storage/blob/main/select-url.md#a-second-example-ad-creative-selection-by-frequency).
373 |
374 | If the logic involes updating data organized across multiple keys, then the lock is likely necessary. [Example: Report on Multiple Keys](#example-report-on-multiple-keys).
375 |
376 | ### Reporting embedder context
377 |
378 | In using the [Private Aggregation API](https://github.com/patcg-individual-drafts/private-aggregation-api) to report on advertisements within [fenced frames](https://github.com/wicg/fenced-frame/), for instance, we might report on viewability, performance, which parts of the ad the user engaged with, the fact that the ad showed up at all, and so forth. But when reporting on the ad, it might be important to tie it to some contextual information from the embedding publisher page, such as an event-level ID.
379 |
380 | In a scenario where the input URLs for the [fenced frame](https://github.com/wicg/fenced-frame/) must be k-anonymous, e.g. if we create a [FencedFrameConfig](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md) from running a [Protected Audience auction](https://github.com/WICG/turtledove/blob/main/FLEDGE.md#2-sellers-run-on-device-auctions), it would not be a good idea to rely on communicating the event-level ID to the [fenced frame](https://github.com/wicg/fenced-frame/) by attaching an identifier to any of the input URLs, as this would make it difficult for any input URL(s) with the attached identifier to reach the k-anonymity threshold.
381 |
382 | Instead, before navigating the [fenced frame](https://github.com/wicg/fenced-frame/) to the auction's winning [FencedFrameConfig](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md) `fencedFrameConfig`, we could write the event-level ID to `fencedFrameConfig` using `fencedFrameConfig.setSharedStorageContext()` as in the example below.
383 |
384 | Subsequently, anything we've written to `fencedFrameConfig` through `setSharedStorageContext()` prior to the fenced frame's navigation to `fencedFrameConfig`, can be read via `sharedStorage.context` from inside a shared storage worklet created by the [fenced frame](https://github.com/wicg/fenced-frame/), or created by any of its same-origin children.
385 |
386 | In the embedder page:
387 |
388 | ```js
389 | // See https://github.com/WICG/turtledove/blob/main/FLEDGE.md for how to write an auction config.
390 | const auctionConfig = { ... };
391 |
392 | // Run a Protected Audience auction, setting the option to "resolveToConfig" to true.
393 | auctionConfig.resolveToConfig = true;
394 | const fencedFrameConfig = await navigator.runAdAuction(auctionConfig);
395 |
396 | // Write to the config any desired embedder contextual information as a string.
397 | fencedFrameConfig.setSharedStorageContext("My Event ID 123");
398 |
399 | // Navigate the fenced frame to the config.
400 | document.getElementById('my-fenced-frame').config = fencedFrameConfig;
401 | ```
402 |
403 | In the fenced frame (`my-fenced-frame`):
404 |
405 | ```js
406 | // Save some information we want to report that's only available inside the fenced frame.
407 | const frameInfo = { ... };
408 |
409 | // Send a report using shared storage and private aggregation.
410 | try {
411 | await window.sharedStorage.worklet.addModule('report.js');
412 | await window.sharedStorage.run('send-report', {
413 | data: { info: frameInfo },
414 | });
415 | } catch (error) {
416 | // Error handling
417 | }
418 | ```
419 |
420 | In the worklet script (`report.js`):
421 |
422 | ```js
423 | class ReportingOperation {
424 | async run(data) {
425 | // Helper functions that map the embedder context to a predetermined bucket and the
426 | // frame info to an appropriately-scaled value.
427 | // See also https://github.com/patcg-individual-drafts/private-aggregation-api#examples
428 | function convertEmbedderContextToBucketId(context) { ... }
429 | function convertFrameInfoToValue(info) { ... }
430 |
431 | // The user agent sends the report to the reporting endpoint of the script's
432 | // origin (that is, the caller of `sharedStorage.run()`) after a delay.
433 | privateAggregation.contributeToHistogram({
434 | bucket: convertEmbedderContextToBucketId(sharedStorage.context) ,
435 | value: convertFrameInfoToValue(data.info)
436 | });
437 | }
438 | }
439 | register('send-report', ReportingOperation);
440 | ```
441 |
442 | ### Keeping a worklet alive for multiple operations
443 |
444 | Callers may wish to run multiple worklet operations from the same context, e.g. they might select a URL and then send one or more aggregatable reports. To do so, they would need to use the `keepAlive: true` option when calling each of the worklet operations (except perhaps in the last call, if there was no need to extend the worklet's lifetime beyond that call).
445 |
446 | ### Writing to Shared Storage via response headers
447 |
448 | For an origin making changes to their Shared Storage data at a point when they do not need to read the data, an alternative to using the Shared Storage JavaScript API is to trigger setter and/or deleter operations via the HTTP response header `Shared-Storage-Write` as in the examples below.
449 |
450 | In order to perform operations via response header, the origin must first opt-in via one of the methods below, causing the HTTP request header `Sec-Shared-Storage-Writable: ?1` to be added by the user agent if the request is eligible based on permissions checks.
451 |
452 | An origin `a.example` could initiate such a request in multiple ways.
453 |
454 | On the client side, to initiate the request:
455 | 1. `fetch()` option:
456 | ```js
457 | fetch("https://a.example/path/for/updates", {sharedStorageWritable: true});
458 | ```
459 | 2. Content attribute option with an iframe (also possible with an img):
460 | ```
461 |
462 |
463 | ```
464 | 3. IDL attribute option with an iframe (also possible with an img):
465 | ```js
466 | let iframe = document.getElementById("my-iframe");
467 | iframe.sharedStorageWritable = true;
468 | iframe.src = "https://a.example/path/for/updates";
469 | ```
470 |
471 | On the server side, here is an example response header:
472 | ```text
473 | Shared-Storage-Write: clear, set;key="hello";value="world";ignore_if_present, append;key="good";value="bye", delete;key="hello", set;key="all";value="done", options;with_lock="lock1"
474 | ```
475 |
476 | Sending the above response header would be equivalent to making the following call on the client side, from either the document or a worklet:
477 | ```js
478 |
479 | sharedStorage.batchUpdate([
480 | new SharedStorageClearMethod(),
481 | new SharedStorageSetMethod("hello", "world", {ignoreIfPresent: true}),
482 | new SharedStorageAppendMethod("good", "bye"),
483 | new SharedStorageDeleteMethod("hello"),
484 | new SharedStorageSetMethod("all", "done")
485 | ], { withLock: "lock1" })
486 |
487 | ```
488 |
489 | ### Loading cross-origin worklet scripts
490 |
491 | There are currently six (6) approaches to creating a worklet that loads cross-origin script. The partition origin for the worklet's shared storage data access depends on the approach.
492 |
493 | #### Using the context origin as data partition origin
494 | The first three (3) approaches use the invoking context's origin as the partition origin for shared storage data access and the invoking context's site for shared storage budget withdrawals.
495 |
496 | 1. Call `addModule()` with a cross-origin script.
497 |
498 | In an "https://a.example" context in the embedder page:
499 |
500 | ```
501 | await sharedStorage.worklet.addModule("https://b.example/worklet.js");
502 | ```
503 |
504 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://a.example" (i.e. the context origin) will be used.
505 |
506 | 2. Call `createWorklet()` with a cross-origin script.
507 |
508 | In an "https://a.example" context in the embedder page:
509 |
510 | ```
511 | const worklet = await sharedStorage.createWorklet("https://b.example/worklet.js");
512 | ```
513 |
514 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://a.example" (i.e. the context origin) will be used.
515 |
516 | 3. Call `createWorklet()` with a cross-origin script, setting its `dataOption` to the invoking context's origin.
517 |
518 | In an "https://a.example" context in the embedder page:
519 |
520 | ```
521 | const worklet = await sharedStorage.createWorklet("https://b.example/worklet.js", {dataOrigin: "context-origin"});
522 | ```
523 |
524 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://a.example" (i.e. the context origin) will be used.
525 |
526 | #### Using the worklet script origin as data partition origin
527 | The fourth approach uses the worklet script's origin as the partition origin for shared storage data access and the worklet script's site for shared storage budget withdrawals.
528 |
529 | 4. Call `createWorklet()` with a cross-origin script, setting its `dataOption` to the worklet script's origin.
530 |
531 | In an "https://a.example" context in the embedder page:
532 |
533 | ```
534 | const worklet = await sharedStorage.createWorklet("https://b.example/worklet.js", {dataOrigin: "script-origin"});
535 | ```
536 |
537 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://b.example" (i.e. the worklet script origin) will be used, assuming that the worklet script's server confirmed opt-in with the required "Shared-Storage-Cross-Origin-Worklet-Allowed: ?1" response header.
538 |
539 | #### Using a custom origin as data partition origin
540 | The fifth through eighth approaches use a custom origin as the partition origin for shared storage data access and the custom origin's site for shared storage budget withdrawals.
541 |
542 | 5. Call `createWorklet()`, setting its `dataOption` to a string whose value is the serialization of the custom origin.
543 |
544 | In an "https://a.example" context in the embedder page:
545 |
546 | ```
547 | const worklet = await sharedStorage.createWorklet("https://a.example/worklet.js", {dataOrigin: "https://custom.example"});
548 | ```
549 |
550 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://custom.example" will be used, assuming that the [/.well-known/](#well-known) JSON file at "https://custom.example/.well-known/shared-storage/trusted-origins" contains an array of dictionaries, where one of its dictionaries has
551 |
552 | * the `scriptOrigin` key's value matches "https://a.example" (i.e. its value is "https://a.example", `"*"`, or an array of strings containing "https://a.example")
553 | * the `contextOrigin` key's value matches "https://a.example" (i.e. its value is "https://a.example", `"*"`, or an array of strings containing "https://a.example")
554 |
555 |
556 | 6. Call `createWorklet()` with a cross-origin script, setting its `dataOption` to a string whose value is the serialization of the custom origin.
557 |
558 | In an "https://a.example" context in the embedder page:
559 |
560 | ```
561 | const worklet = await sharedStorage.createWorklet("https://b.example/worklet.js", {dataOrigin: "https://custom.example"});
562 | ```
563 |
564 | For any subsequent `run()` or `selectURL()` operation invoked on this worklet, the shared storage data for "https://custom.example" will be used, assuming that the [/.well-known/](#well-known) JSON file at "https://custom.example/.well-known/shared-storage/trusted-origins" contains an array of dictionaries, where one of its dictionaries has
565 |
566 | * the `scriptOrigin` key's value matches "https://b.example" (i.e. its value is "https://b.example", `"*"`, or an array of strings containing "https://b.example")
567 | * the `contextOrigin` key's value matches "https://a.example" (i.e. its value is "https://a.example", `"*"`, or an array of strings containing "https://a.example")
568 |
569 |
570 |
571 | ## Error handling
572 | Note that the shared storage APIs may throw for several possible reasons. The following list of situations is not exhaustive, but, for example, the APIs may throw if the site invoking the API is not [enrolled](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and/or [attested](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations), if the user has disabled shared storage in site settings, if the "shared-storage" or "shared-storage-select-url" permissions policy denies access, or if one of its arguments is invalid.
573 |
574 | We recommend handling exceptions. This can be done by wrapping `async..await` calls to shared storage JS methods in `try...catch` blocks, or by following calls that are not awaited with `.catch`:
575 |
576 | ```js
577 | try {
578 | await window.sharedStorage.worklet.addModule('worklet.js');
579 | } catch (error) {
580 | // Handle error.
581 | }
582 | ```
583 |
584 | ```js
585 | window.sharedStorage.worklet.addModule('worklet.js')
586 | .catch((error) => {
587 | // Handle error.
588 | });
589 | ```
590 | ## Worklets can outlive the associated document
591 |
592 | After a document dies, the corresponding worklet (if running an operation) will continue to be kept alive for a maximum of two seconds to allow the pending operation(s) to execute. This gives more confidence that any end-of-page operations (e.g. reporting) are able to finish.
593 |
594 | ## Permissions Policy
595 |
596 | Shared storage methods can be disallowed by the "shared-storage" [policy-controlled feature](https://w3c.github.io/webappsec-permissions-policy/#policy-controlled-feature). Its default allowlist is * (i.e. every origin). APIs built on top of Shared Storage have their own specific permission policies, so it is possible to allow reading and writing of Shared Storage while disabling particular APIs.
597 |
598 | ### Permissions Policy inside the shared storage worklet
599 | The permissions policy inside the shared storage worklet will inherit the permissions policy of the associated document.
600 |
601 | ## Data Retention Policy
602 | Each key is cleared after thirty days of last write (`set` or `append` call). If `ignoreIfPresent` is true, the last write time is updated.
603 |
604 | ## Data Storage Limits
605 | Shared Storage is not subject to the quota manager, as that would leak information across sites. Therefore we limit the per-origin total key and value bytes to 5MB.
606 |
607 |
608 | ## Privacy
609 |
610 | Shared Storage takes the following protective measures to prevent its stored data from being read by means other than via approved APIs (e.g., via side channels):
611 |
612 | - **Concealed Operation Time and Errors**: When writing data or running worklet operations from the Window scope, the method returns immediately and will not expose errors that might arise from reading shared storage data.
613 |
614 | - **Disabled Storage Access before Loading Finishes**: Access to Shared Storage is disabled until a module script finishes loading. This prevents websites from using timing attacks to learn about the data stored in Shared Storage.
615 |
616 | - **Isolated Locks**: Locks requested for Shared Storage are completely separate from locks requested from the Window scope. This prevents information leakage through lock contention.
617 |
618 | ### Privacy-Preserving APIs
619 |
620 | The APIs that can read data from Shared Storage have their own privacy documentation.
621 |
622 | ### Enrollment and Attestation
623 | Use of Shared Storage requires [enrollment](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attestation](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations) via the [Privacy Sandbox enrollment attestation model](https://github.com/privacysandbox/attestation/blob/main/README.md).
624 |
625 | For each method in the Shared Storage API surface, a check will be performed to determine whether the calling [site](https://html.spec.whatwg.org/multipage/browsers.html#site) is [enrolled](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attested](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations). In the case where the [site](https://html.spec.whatwg.org/multipage/browsers.html#site) is not [enrolled](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attested](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations), the promise returned by the method is rejected.
626 |
627 |
628 | ## Possibilities for extension
629 |
630 | ### Interactions between worklets
631 |
632 | Communication between worklets is not possible in the initial design. However, adding support for this would enable multiple origins to flexibly share information without needing a dedicated origin for that sharing. Relatedly, allowing a worklet to create other worklets might be useful.
633 |
634 |
635 | ### Registering event handlers
636 |
637 | We could support event handlers in future iterations. For example, a handler could run a previously registered operation when a given key is modified (e.g. when an entry is updated via a set or append call):
638 |
639 |
640 | ```js
641 | sharedStorage.addEventListener(
642 | 'key' /* event_type */,
643 | 'operation-to-run' /* operation_name */,
644 | { key: 'example-key', actions: ['set', 'append'] } /* options */);
645 | ```
646 |
647 |
648 |
649 | ## Acknowledgements
650 |
651 | Many thanks for valuable feedback and advice from:
652 |
653 | Victor Costan,
654 | Christian Dullweber,
655 | Charlie Harrison,
656 | Jeff Kaufman,
657 | Rowan Merewood,
658 | Marijn Kruisselbrink,
659 | Nasko Oskov,
660 | Evgeny Skvortsov,
661 | Michael Tomaine,
662 | David Turner,
663 | David Van Cleve,
664 | Zheng Wei,
665 | Mike West.
666 |
--------------------------------------------------------------------------------
/select-url.md:
--------------------------------------------------------------------------------
1 | # selectURL API Explainer
2 |
3 | For browser users that have third-party cookies disabled, third parties on the page might still want to select content to show those users based on cross-site data in a privacy-positive way. For instance, they may want to a/b test their third-party embed consistently for a user across sites. Or, they may want to show a different login button for users that are known to have an account vs those that don’t.
4 |
5 | The `selectURL` API is designed for such use cases. It allows the caller to choose between a set of URLs based on third-party data. The API is built on top of [shared storage](https://github.com/WICG/shared-storage) and uses a shared storage worklet to read the available cross-site data and select one of the given URLs. The selected URL is returned to the caller in an opaque fashion, such that it can’t be read except within a [fenced frame](https://github.com/WICG/fenced-frame/tree/master/explainer).
6 |
7 | This means that the selected URL needs to be fenced frame compatible, and not communicate with the page it’s embedded on, save for say a click notification.
8 |
9 |
10 |
11 | ## Simple example: Consistent A/B experiments across sites
12 |
13 | A third-party, `a.example`, wants to randomly assign users to different groups (e.g. experiment vs control) in a way that is consistent cross-site.
14 |
15 | To do so, `a.example` writes a seed to its shared storage (which is not added if already present). `a.example` then registers and runs an operation in the shared storage [worklet](https://developer.mozilla.org/en-US/docs/Web/API/Worklet) that assigns the user to a group based on the seed and the experiment name and chooses the appropriate ad for that group.
16 |
17 | In an `a.example` document:
18 |
19 |
20 | ```js
21 | function generateSeed() { … }
22 | try {
23 | await window.sharedStorage.worklet.addModule('experiment.js');
24 |
25 | // Only write a cross-site seed to a.example's storage if there isn't one yet.
26 | window.sharedStorage.set('seed', generateSeed(), { ignoreIfPresent: true });
27 |
28 | // Fenced frame config contains an opaque form of the URL (urn:uuid) that is created by
29 | // privileged code to avoid leaking the chosen input URL back to the document.
30 |
31 | const fencedFrameConfig = await window.sharedStorage.selectURL(
32 | 'select-url-for-experiment',
33 | [
34 | {url: "blob:https://a.example/123…", reportingMetadata: {"click": "https://report.example/1..."}},
35 | {url: "blob:https://b.example/abc…", reportingMetadata: {"click": "https://report.example/a..."}},
36 | {url: "blob:https://c.example/789…"}
37 | ],
38 | {
39 | data: { name: 'experimentA' },
40 | resolveToConfig: true
41 | }
42 | );
43 |
44 | document.getElementById('my-fenced-frame').config = fencedFrameConfig;
45 | } catch (error) {
46 | // Error handling
47 | }
48 | ```
49 |
50 |
51 | Worklet script (i.e. `experiment.js`):
52 |
53 |
54 | ```js
55 | class SelectURLOperation {
56 | hash(experimentName, seed) { … }
57 |
58 | async run(urls, data) {
59 | const seed = await sharedStorage.get('seed');
60 | return hash(data.name, seed) % urls.length;
61 | }
62 | }
63 | register('select-url-for-experiment', SelectURLOperation);
64 | ```
65 |
66 | ## Demonstration
67 |
68 | You can [try it out](https://shared-storage-demo.web.app/) using Chrome 104+ (currently in canary and dev channels as of June 7th 2022).
69 |
70 |
71 |
72 | ## Fenced frame enforcement
73 |
74 | The usage of fenced frames with the URL Selection operation will not be required until at least 2026. We will provide significant advanced notice before the fenced frame usage is required. Until 2026, you are free to use an iframe with URL Selection instead of a fenced frame.
75 |
76 | To use an iframe, omit passing in the `resolveToConfig` flag or set it to `false`, and set the returned opaque URN to the `src` attribute of the iframe.
77 |
78 | ```js
79 | try {
80 | const opaqueURN = await window.sharedStorage.selectURL(
81 | 'select-url-for-experiment',
82 | {
83 | data: { ... }
84 | }
85 | );
86 |
87 | document.getElementById('my-iframe').src = opaqueURN;
88 | } catch (error) {
89 | // Error handling
90 | }
91 | ```
92 |
93 | ## Proposed API surface
94 |
95 | `window.sharedStorage.worklet.selectURL(name, urls, options)`
96 |
97 | * The `name` and `options` parameters are similar to those found in `window.sharedStorage.worklet.run`. The primary difference is the urls input parameter which lists the URLs to select from, and the fact that the worklet operation must choose one of them by returning an integer index.
98 | * `urls` is a list of dictionaries, each containing a candidate URL `url` and optional reporting metadata (a dictionary, with the key being the event type and the value being the reporting URL; identical to Protected Audience's [registerAdBeacon()](https://github.com/WICG/turtledove/blob/main/Fenced_Frames_Ads_Reporting.md#registeradbeacon) parameter), with a max length of 8.
99 |
100 | * The `url` of the first dictionary in the list is the `default URL`. This is selected if there is a script error, or if there is not enough budget remaining.
101 |
102 | * `savedQuery` (a string) is the name of a query to be saved or reused on the same page load. The first time `selectURL` is run with a `savedQuery` name on a page, the returned index will be remembered and associated with that name. Subsequent calls with the same name will return the same index (but it can be for a different set of URLs). Note, saved queries are stored per-page-load but work across frames on the same page.
103 | * If the value of `savedQuery` is nonempty and has not previously been associated with a [result index](#result-index) for call to `selectURL()` on the same page, and if the call to `selectURL()` succeeds:
104 | * The pair of (`savedQuery`, [`index`](#result-index)) will be stored for the lifetime of the page.
105 | * The shared storage data origin's site can reuse the query from anywhere within the page.
106 | * If the value of `savedQuery` is nonempty and has previously been associated with a [result index](#result-index) for a call to `selectURL()` on the same page, then:
107 | * Instead of running the registered JavaScript operation, `selectURL()` will use the stored [result index](#result-index) associated with the value of `savedQuery` to choose the selected URL.
108 | * The [short-term per-page budgets](#Short-Term-Budgets) will not be charged.
109 |
110 | * The reporting metadata will be used in the short-term to allow event-level reporting via `window.fence.reportEvent()` as described in the [Protected Audience explainer](https://github.com/WICG/turtledove/blob/main/Fenced_Frames_Ads_Reporting.md).
111 |
112 |
113 | * There will be a per-[site](https://html.spec.whatwg.org/multipage/browsers.html#site) (the site of the Shared Storage worklet) budget for `selectURL`. This is to limit the rate of leakage of cross-site data learned from the selectURL to the destination pages that the resulting Fenced Frames navigate to. Each time a Fenced Frame navigates the top frame, for each `selectURL()` involved in the creation of the Fenced Frame, log(|`urls`|) bits will be deducted from the corresponding [site](https://html.spec.whatwg.org/multipage/browsers.html#site)’s budget. At any point in time, the current budget remaining will be calculated as `max_budget - sum(deductions_from_last_24hr)`
114 |
115 | * The promise resolves to a [fenced frame config](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md) only when the `resolveToConfig` property is set to `true`. If the property is set to `false` or not set, the promise resolves to an opaque URN that can be rendered by an iframe.
116 |
117 | * For the associated operation in the worklet to work with `sharedStorage.selectURL()`, `run()` should take `data` and `urls` as arguments and return the index of the selected URL. Any invalid return value is replaced with a [default return value](#default).
118 |
119 |
120 | ## A second example, ad creative selection by frequency
121 |
122 | If an ad creative has been shown to the user too many times, a different ad should be selected.
123 |
124 | In the advertiser's iframe:
125 |
126 | ```js
127 | // Fetches two ads in a list. The second is the proposed ad to display, and the first
128 | // is the fallback in case the second has been shown to this user too many times.
129 | const ads = await advertiser.getAds();
130 |
131 | try {
132 | // Register the worklet module
133 | await window.sharedStorage.worklet.addModule('creative-selection-by-frequency.js');
134 |
135 | // Run the URL selection operation
136 | const frameConfig = await window.sharedStorage.selectURL(
137 | 'creative-selection-by-frequency',
138 | ads.urls,
139 | {
140 | data: {
141 | campaignId: ads.campaignId
142 | },
143 | resolveToConfig: true,
144 | });
145 |
146 | // Render the frame
147 | document.getElementById('my-fenced-frame').config = frameConfig;
148 | } catch (error) {
149 | // Error handling
150 | }
151 | ```
152 |
153 | In the worklet script (`creative-selection-by-frequency.js`):
154 |
155 | ```js
156 | class CreativeSelectionByFrequencyOperation {
157 | async run(urls, data) {
158 | // By default, return the default url (0th index).
159 | let index = 0;
160 |
161 | // Acquire a lock to ensure that the count is accurate even if multiple
162 | // instances of the code are running concurrently (e.g. in separate tabs).
163 | await navigator.locks.request("creation-selection-by-frequency-lock", async (lock) => {
164 | let count = await sharedStorage.get(data.campaignId);
165 | count = count ? parseInt(count) : 0;
166 |
167 | // If under cap, return the desired ad.
168 | if (count < 3) {
169 | index = 1;
170 | sharedStorage.set(data.campaignId, (count + 1).toString());
171 | }
172 | });
173 |
174 | return index;
175 | }
176 | }
177 |
178 | register('creative-selection-by-frequency', CreativeSelectionByFrequencyOperation);
179 | ```
180 |
181 |
182 | ## Permissions Policy
183 |
184 | The sharedStorage.selectURL() method can be disallowed by the "shared-storage-select-url" [policy-controlled feature](https://w3c.github.io/webappsec-permissions-policy/#policy-controlled-feature). Its default allowlist is * (i.e. every origin).
185 |
186 |
187 | ## Dependencies
188 |
189 | This API is dependent on the following other proposals:
190 |
191 |
192 |
193 | * Shared Storage to read and write cross-site data in a private manner.
194 | * [Fenced frames](https://github.com/WICG/fenced-frame) (and the associated concept of [fenced frame configs](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md)) to render the chosen URL without leaking the choice to the top-level document.
195 |
196 |
197 | ## Privacy
198 | The worklet selects from a small list of (up to 8) URLs, each in its own dictionary with optional reporting metadata. The chosen URL is stored in a fenced frame config as an opaque form that can only be read by a [fenced frame](https://github.com/WICG/fenced-frame); the embedder does not learn this information. The chosen URL represents up to log2(num urls) bits of cross-site information (as measured according to [information theory](https://en.wikipedia.org/wiki/Entropy_(information_theory))). Once the Fenced Frame receives a user gesture and navigates to its destination page, the information within the fenced frame leaks to the destination page. To limit the rate of leakage of this data, there is a bit budget applied to the output API. If the budget is exceeded, the selectURL() will return the default (0th index) URL.
199 |
200 | selectURL() can be called in a top-level fenced frame, but not from within a nested fenced frame. This is to prevent leaking lots of bits all at once via selectURL() chaining (i.e. a fenced frame can call selectURL() to add a few more bits to the fenced frame's current URL and render the result in a nested fenced frame). Use cases that will benefit from selectURL() being allowed from inside the top level fenced frame: [issue](https://github.com/WICG/fenced-frame/issues/44).
201 |
202 | ## Budgeting
203 | The rate of leakage of cross-site data need to be constrained. Therefore, we propose that there be a daily budget on how many bits of cross-site data can be leaked by the API per [site](https://html.spec.whatwg.org/multipage/browsers.html#site). Note that each time a Fenced Frame is clicked on and navigates the top frame, up to log2(|urls|) [bits of information](https://en.wikipedia.org/wiki/Entropy_(information_theory)) can potentially be leaked for each selectURL() involved in the creation of the Fenced Frame. Therefore, Shared Storage will deduct that log2(|urls|) bits from the Shared Storage worklet's [site](https://html.spec.whatwg.org/multipage/browsers.html#site)'s budget at that point. If the sum of the deductions from the last 24 hours exceed a threshold, then further selectURL()s will return the default value (the first url in the list) until some budget is freed up.
204 |
205 | Why do we assume that log2(|urls|) bits of cross-site information are leaked by a call to `selectURL`? Because the embedder (the [site](https://html.spec.whatwg.org/multipage/browsers.html#site) calling `selectURL`) is providing a list of urls to choose from using cross-site information. If `selectURL` were abused to leak the first few bits of the user's cross-site identity, then, with 8 URLs to choose from, they could leak the first 3 bits of the id (e.g., imagine urls: https://example.com/id/000, https://example.com/id/001, https://example.com/id/010, ..., https://example.com/id/111). One can leak at most log2(|urls|) bits, and so that is what we deduct from the budget, but only after the fenced frame navigates the top page which is when its data can be communicated.
206 |
207 | #### Budget Details
208 | The budgets for bits of entropy for selectURL are as follows.
209 |
210 | ##### Long Term Budget
211 |
212 | In the long term, `selectURL()` will leak bits of entropy on top-level navigation (e.g., a tab navigates). Therefore it is necessary to impose a budget for this leakage.
213 |
214 | * There is a 12 bit daily per-[site](https://html.spec.whatwg.org/multipage/browsers.html#site) budget for `selectURL()`, to be queried on each `selectURL()` call for sufficient budget and charged on navigation. This is subject to change.
215 | * The cost of a `selectURL()` call is log2(number of urls to `selectURL()` call) bits. This cost is only logged once the fenced frame holding the selected URL navigates the top frame. e.g., if the fenced frame can't communicate its contents (doesn't navigate), then there is no budget cost for that call to`selectURL()`.
216 | * The remaining budget at any given time for a [site](https://html.spec.whatwg.org/multipage/browsers.html#site) is 12 - (the sum of the log of budget deductions from the past 24 hours).
217 | * If the remaining budget is less than log2(number of urls in `selectURL()` call), the default URL is returned and 1 bit is logged if the fenced frame is navigated.
218 |
219 | ##### Short Term Budgets
220 |
221 | In the short term, we have event-level reporting and less-restrictive [fenced frames](https://github.com/WICG/fenced-frame), which allow further leakage; thus it is necessary to impose additional limits. On top of the navigation bit budget described above, there will be two more budgets, each maintained on a per top-level navigation basis. The bit values for each call to `selectURL()` are calculated in the same way as detailed for the navigation bit budget.
222 |
223 | * Each page load will have a per-[site](https://html.spec.whatwg.org/multipage/browsers.html#site) bit budget of 6 bits for `selectURL()` calls. At the start of a new top-level navigation, this budget will refresh. Saved queries named with the `savedQuery` option will only be charged against the budget on their initial use, not on any subsequent re-uses within the same page load.
224 | * Each page load will also have an overall bit budget of 12 bits for `selectURL()`. This budget will be contributed to by all sites on the page. As with the per-[site](https://html.spec.whatwg.org/multipage/browsers.html#site) per-page load bit budget, this budget will refresh when the top frame navigates, and saved queries named with the `savedQuery` option will only be charged against the budget on their initial use, not on any subsequent re-uses within the same page load.
225 |
226 | ```js
227 | try {
228 | // Assuming that this call to `selectURL()` is the first to use
229 | // `savedQuery: "control_or_experiment"` on this page, this call
230 | // will be charged to both of the per-page budgets.
231 | const config1 = await sharedStorage.selectURL("experiment", urls1, {savedQuery: "control_or_experiment", keepAlive: true, resolveToConfig: true});
232 | document.getElementById("my-fenced-frame1").config = config1;
233 | // This next call will not be charged to either of the
234 | // per-page budgets.
235 | const config2 = await sharedStorage.selectURL("experiment", urls2, {savedQuery: "control_or_experiment", resolveToConfig: true});
236 | document.getElementById("my-fenced-frame2").config = config2;
237 | } catch(error) {
238 | // Error handling
239 | }
240 | ```
241 |
242 | ## Enrollment and Attestation
243 | Use of selectURL requires shared storage [enrollment](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attestation](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations) via the [Privacy Sandbox enrollment attestation model](https://github.com/privacysandbox/attestation/blob/main/README.md).
244 |
245 | A check will be performed to determine whether the calling [site](https://html.spec.whatwg.org/multipage/browsers.html#site) is [enrolled](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attested](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations). In the case where the [site](https://html.spec.whatwg.org/multipage/browsers.html#site) is not [enrolled](https://github.com/privacysandbox/attestation/blob/main/how-to-enroll.md) and [attested](https://github.com/privacysandbox/attestation/blob/main/README.md#core-privacy-attestations), the promise returned by the method is rejected.
246 |
247 |
248 | ## Event Level Reporting
249 | In the long term we'd like all reporting via selectURL to happen via the Private Aggregation output API (or some additional noised reporting gate). We understand that in the short term it may be necessary for the industry to continue to use event-level reporting as they transition to more private reporting. Event-level reporting for content selection (`selectURL()`) will be available until at least 2026, and we will provide substantial notice for developers before the transition takes place.
250 |
251 | Event level reports work in a way similar to how they work in Protected Audience. First, when calling selectURL, the caller adds a `reportingMetadata` optional dict to the URLs that they wish to send reports for, such as:
252 | ```javascript
253 | sharedStorage.selectURL(
254 | "test-url-selection-operation",
255 | [{url: "fenced_frames/title0.html"},
256 | {url: "fenced_frames/title1.html",
257 | reportingMetadata: {'click': "fenced_frames/report1.html",
258 | 'visible': "fenced_frames/report2.html"}}]);
259 | ```
260 | In this case, when in the fenced frame, event types are defined for `click` and `visibility`. Once the fenced frame is ready to send a report, it can call something like:
261 |
262 | ```javascript
263 | window.fence.reportEvent({eventType: 'visible',
264 | eventData: JSON.stringify({'duration': duration}),
265 | destination: ['shared-storage-select-url']});
266 | ```
267 | and it will send a POST message with the eventData. See the [fenced frame reporting document](https://github.com/WICG/turtledove/blob/main/Fenced_Frames_Ads_Reporting.md) for more details.
268 |
269 | ## Default values
270 |
271 | When `sharedStorage.selectURL()` doesn’t return a valid output (including throwing an error), the user agent returns the first default URL, to prevent information leakage. For `sharedStorage.run()`, there is no output, so any return value is ignored.
272 |
273 | ## Preventing timing attacks
274 |
275 | Revealing the time an operation takes to run could also leak information. We avoid this by having `sharedStorage.selectURL()` immediately return the promise which later resolves into an [fenced frame config](https://github.com/WICG/fenced-frame/blob/master/explainer/fenced_frame_config.md) that contains the opaque URL that is mapped to the selected URL once the operation completes. A Fenced Frame can be created with the returned fenced frame config even before the selectURL operation has completed. The frame will wait for it to complete first. Similarly, outside a worklet, `set()`, `remove()`, etc. return promises that resolve after queuing the writes. Inside a worklet, these writes join the same queue but their promises only resolve after completion.
276 |
--------------------------------------------------------------------------------
/shared-storage-tester-list.md:
--------------------------------------------------------------------------------
1 | # Shared Storage Tester List
2 |
3 | ## Ecosystem Testing of Shared Storage
4 |
5 | The purpose of this page is to consolidate testing information which is currently distributed across various GitHub issues, company blogs, social posts, etc.
6 | The usefulness of this page depends on testers sharing information and updates; if testers do not contribute or the information becomes stale it would make sense to deprecate.
7 |
8 | ## Disclaimers
9 |
10 | - Not a complete list. Testers are strongly encouraged to share their activities and insights publicly for the benefit of the broader community, but sharing is voluntary and therefore this page is not expected to reflect all testing activity.
11 |
12 | - Not evaluative. The purpose of this page is to consolidate links to information published by Privacy Sandbox testers. Editors will review submissions for relevance and to ensure general conformance to the guidelines above, but are not evaluating or endorsing the information provided.
13 |
14 | - Editors will regularly review and approve submissions that meet the guidelines below. If you believe that an error has been submitted, please create an issue in the shared storage repository with the words ‘[Tester List]‘ in the subject and the Editors will respond in short order.
15 |
16 | ## Guidelines
17 |
18 | - Enter information on behalf of your own organization.
19 |
20 | - Do not share test results or other detailed information inline; instead link to GitHub issues or other public pages for elaboration and discussion.
21 |
22 | - Table fields:
23 | - Company/Party: The organization directly conducting tests or analysis.
24 | - Role in Testing: Organization’s role with respect to testing shared storage. For example, SSP, DSP, publisher, type of embedded service etc.
25 | - Est. Testing Timeframe: Expected start and duration of tests, if known. If still scoping, indicate 'TBD - Scoping' in column.
26 | - [Optional] Link to Testing Plan and/or Learnings: Link to GitHub issues, blog posts or other public information. This could include plans for upcoming tests, or insights and summaries from completed tests.
27 | - [Optional] How to Contact You: For example, a website form or reply on a GitHub issue.
28 |
29 | ## How to submit
30 |
31 | 1. On the shared storage GitHub page, navigate to the document in the main table called ‘shared-storage-tester-list.md’
32 |
33 | 1. Click the ‘pencil’ icon on the right side to edit the table and add your information
34 | 1. Use the | to make sure that the information that you provide correctly shows up in each cell
35 | 1. After you select ‘Propose changes’ an editor will review the edits and add your information in the coming days.
36 |
37 | 1. You can find additional details here for [editing tables in GitHub](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables).
38 |
39 | ## Table
40 |
41 | | Company / Party | Industry or vertical | Est. Testing Timeframe | Link to testing plan and/or learnings | How to contact you |
42 | | --------------- | -------------------- | ---------------------- | ------------------------------------- | ------------------ |
43 | | Google Ad Manager + AdSense | SSP | Testing ongoing | | Publishers with questions should reach out via their account manager directly, or via our [support channels](https://support.google.com/admanager/gethelp).|
44 | | Google Ads Unique Reach | Metrics | Testing ongoing | | https://support.google.com/google-ads/gethelp |
45 | | Criteo | DSP | Testing ongoing | | privacy-sandbox-testing@criteo.com |
46 | | NextRoll | DSP | 2024-01-30 | | privacysandbox@nextroll.com |
47 | | CyberAgent(Dynalyst) | DSP | Testing ongoing | | privacysandbox@cyberagent.co.jp |
48 | | Raptive | Ad Network | Testing Ongoing | datascience@adthrive.com |
49 | | Optable | Adtech services | Testing ongoing | privacysandbox@optable.co |
50 |
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "group": [80485]
3 | , "contacts": ["yoavweiss"]
4 | , "repo-type": "cg-report"
5 | }
6 |
--------------------------------------------------------------------------------