forked from privacycg/storage-access
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstorage-access.bs
404 lines (312 loc) · 24.4 KB
/
storage-access.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
<pre class="metadata">
Title: The Storage Access API
Shortname: storage-access
Repository: privacycg/storage-access
URL: https://privacycg.github.io/storage-access/
Editor: Johann Hofmann, w3cid 120436, Mozilla https://www.mozilla.org, jhofmann@mozilla.com
Editor: Theresa O’Connor, w3cid 40614, Apple Inc. https://apple.com, hober@apple.com
Editor: John Wilander, w3cid 89478, Apple Inc. https://apple.com, wilander@apple.com
Abstract: The Storage Access API enables content in iframes to request access to website data (such as cookies).
Status Text: This specification is intended to be merged into the HTML Living Standard. It is neither a WHATWG Living Standard nor is it on the standards track at W3C.
Text Macro: LICENSE <a href=https://creativecommons.org/licenses/by/4.0/>Creative Commons Attribution 4.0 International License</a>
Group: privacycg
Status: CG-DRAFT
Level: None
Markup Shorthands: markdown yes, css no
Complain About: accidental-2119 true
</pre>
<!-- File issues on HTML to export each of these -->
<pre class=link-defaults>
spec:html; type:dfn; text:session history; url:https://html.spec.whatwg.org/multipage/history.html#session-history
spec:html; type:dfn; text:current entry; url:https://html.spec.whatwg.org/multipage/history.html#current-entry
spec:webidl; type:dfn; text:resolve
</pre>
<pre class="anchors">
urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT
text: agent cluster; url: #sec-agent-clusters; type: dfn
urlPrefix: https://infra.spec.whatwg.org/; spec: INFRA
text: implementation-defined; url: #implementation-defined; type: dfn
urlPrefix: https://w3c.github.io/webdriver/webdriver-spec.html#; spec: webdriver
type: dfn
text: current browsing context; url: dfn-current-browsing-context
text: WebDriver error; url: dfn-error
text: WebDriver error code; url: dfn-error-code
text: extension command; url: dfn-extension-commands
text: extension command URI template; url: dfn-extension-command-uri-template
text: getting a property; url: dfn-getting-properties
text: invalid argument; url: dfn-invalid-argument
text: local end; url: dfn-local-end
text: remote end steps; url: dfn-remote-end-steps
text: unknown error; url: dfn-unknown-error
text: unsupported operation; url: dfn-unsupported-operation
text: session; url: dfn-session
text: success; url: dfn-success
</pre>
<pre class=biblio>
{
"STORAGE-ACCESS-INTRO": {
"authors": ["John Wilander"],
"date": "February 2018",
"href": "https://webkit.org/blog/8124/introducing-storage-access-api/",
"publisher": "WebKit",
"rawDate": "2018-02-21",
"status": "Blog post",
"title": "Introducing Storage Access API"
}
}
</pre>
<style>
.XXX {
color: #E50000;
font-weight: bold;
}
.XXX::before {
content: "TODO: ";
}
</style>
<section class="non-normative">
<h2 id="intro">Introduction</h2>
<em>This section is non-normative.</em>
User Agents sometimes prevent content inside certain <{iframe}>s from accessing data stored in client-side storage mechanisms like cookies. This can break embedded content which relies on having access to client-side storage.
The Storage Access API enables content inside <{iframe}>s to request and be granted access to their client-side storage, so that embedded content which relies on having access to client-side storage can work in such User Agents. [[STORAGE-ACCESS-INTRO]]
</section>
<h2 id="infra">Infrastructure</h2>
This specification depends on the Infra standard. [[!INFRA]]
<h2 id="the-storage-access-api">The Storage Access API</h2>
This specification defines a method to query whether or not a {{Document}} currently has access to its [=unpartitioned data=] ({{Document/hasStorageAccess()}}), and a method that can be used to request access to its [=unpartitioned data=] ({{Document/requestStorageAccess()}}).
<div class=example>
Alex visits `https://social.example/`. The page sets a cookie. This cookie has been set in a [=first-party-site context=].
Later on, Alex visits `https://video.example/`, which has an <{iframe}> on it which loads `https://social.example/heart-button`. In this case, the `social.example` {{Document}} |doc| is in a [=third party context=], and the cookie set previously might or might not be visible from |doc|`.`{{Document/cookie}}, depending on User Agent storage access policies.
Script in the <{iframe}> can call |doc|`.`{{Document/hasStorageAccess()}} to determine if it has access to the cookie. If it does not have access, it can request access by calling |doc|`.`{{Document/requestStorageAccess()}}.
</div>
<dfn>Unpartitioned data</dfn> is client-side storage that would be available to a [=site=] were it loaded in a [=first-party-site context=].
A {{Document}} is in a <dfn>first-party-site context</dfn> if it is the [=active document=] of a [=top-level browsing context=]. Otherwise, it is in a [=first-party-site context=] if it is an [=active document=] and the [=environment settings object/origin=] and [=top-level origin=] of its [=relevant settings object=] are [=same site=] with one another.
A {{Document}} is in a <dfn>third party context</dfn> if it is not in a [=first-party-site context=].
ISSUE(10): If we let nested <{iframe}>s use this API, we may have to revisit these definitions.
<h3 id="ua-state">User Agent state related to storage access</h3>
A <dfn>storage access map</dfn> is a [=map=] whose keys are [=partitioned storage keys=] and whose values are [=storage access flag sets=].
User Agents maintain a single <dfn>global storage access map</dfn>.
ISSUE(2): What is the lifecycle of the [=global storage access map=]? How long do we remember its contents? Firefox and Safari differ here.
ISSUE(5): When do we age out entries in the [=global storage access map=]? See also [Scope of Storage Access](https://github.com/privacycg/storage-access#scope-of-storage-access).
Each [=agent cluster=] has a <dfn for="agent cluster">storage access map</dfn>.
When an [=agent cluster=] is created, its [=agent cluster/storage access map=] is initialized with a [=map/clone=] of the [=global storage access map=].
To <dfn type="abstract-op">obtain the storage access map</dfn> for a {{Document}} |doc|, run the following steps:
1. Return the [=agent cluster/storage access map=] of |doc|'s [=relevant agent=]'s [=agent cluster=].
A <dfn>partitioned storage key</dfn> is a [=tuple=] consisting of a <dfn for="partitioned storage key">top-level site</dfn> and an <dfn for="partitioned storage key">embedded site</dfn> (both [=sites=]).
<div class=example>
`(("https", "news.example"), ("https", "social.example"))` is a [=partitioned storage key=] whose [=top-level site=] is `("https", "news.example")` and whose [=embedded site=] is `("https", "social.example")`.
</div>
To <dfn type="abstract-op">generate a partitioned storage key</dfn> for a {{Document}} |doc|, run the following steps:
1. Let |settings| be |doc|'s [=relevant settings object=].
1. Let |site| be the result of [=obtain a site|obtaining a site=] from |settings|' [=environment settings object/origin=].
1. If |doc|'s [=Document/browsing context=] is a [=top-level browsing context=], return the [=partitioned storage key=] (|site|, |site|).
1. Let |top-level site| be the result of [=obtain a site|obtaining a site=] from |settings|' [=top-level origin=].
1. Return the [=partitioned storage key=] (|top-level site|, |site|).
A <dfn>storage access flag set</dfn> is a set of zero or more of the following flags, which are used to gate access to client-side storage for |embedded site| when loaded in a [=third party context=] on |top-level site|:
: The <dfn for="storage access flag set" id=has-storage-access-flag>has storage access flag</dfn>
:: When set, this flag indicates |embedded site| has access to its [=unpartitioned data=] when it's loaded in a [=third party context=] on |top-level site|.
: The <dfn for="storage access flag set" id=was-expressly-denied-storage-access-flag>was expressly denied storage access flag</dfn>
:: When set, this flag indicates that the user expressly denied |embedded site| access to its [=unpartitioned data=] when it's loaded in a [=third party context=] on |top-level site|.
To <dfn type="abstract-op">obtain a storage access flag set</dfn> for a [=partitioned storage key=] |key| from a [=/storage access map=] |map|, run the following steps:
1. If |map|[|key|] [=map/exists|does not exist=], run these steps:
1. Let |flags| be a new [=storage access flag set=].
1. [=map/Set=] |map|[|key|] to |flags|.
1. Return |map|[|key|].
To <dfn type="abstract-op">save the storage access flag set</dfn> for a [=partitioned storage key=] |key| in a [=/storage access map=] |map|, run the following steps:
1. [=map/Set=] [=global storage access map=][|key|] to |map|[|key|].
<h3 id="the-document-object">Changes to {{Document}}</h3>
<pre class="idl">
partial interface Document {
Promise<boolean> hasStorageAccess();
Promise<undefined> requestStorageAccess();
};
</pre>
When invoked on {{Document}} |doc|, the <dfn export method for=Document><code>hasStorageAccess()</code></dfn> method must run these steps:
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess -->
<!-- https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/DocumentStorageAccess.cpp#L80 -->
<!-- https://hg.mozilla.org/mozilla-central/file/tip/dom/base/Document.cpp#l15512 -->
1. Let |p| be [=a new promise=].
1. If |doc|'s [=Document/origin=] is an [=opaque origin=], [=/resolve=] |p| with false and return |p|.
1. If |doc|'s [=Document/browsing context=] is a [=top-level browsing context=], [=/resolve=] |p| with true and return |p|.
1. If the [=top-level origin=] of |doc|'s [=relevant settings object=] is an [=opaque origin=], [=/resolve=] |p| with false and return |p|. <!-- https://github.com/privacycg/storage-access/issues/40 -->
1. If |doc|'s [=Document/origin=] is [=same origin=] with the [=top-level origin=] of |doc|'s [=relevant settings object=], [=/resolve=] |p| with true and return |p|.
1. Let |key| be the result of [=generate a partitioned storage key|generating a partitioned storage key=] from |doc|.
1. If |key| is failure, [=resolve=] |p| with false and return |p|.
1. Let |global| be |doc|'s [=relevant global object=].
1. Run these steps [=in parallel=]:
1. Let |map| be the result of [=obtain the storage access map|obtaining the storage access map=] for |doc|.
1. Let |flag set| be the result of [=obtain a storage access flag set|obtaining the storage access flag set=] with |key| from |map|.
1. If |flag set|'s [=was expressly denied storage access flag=] is set, [=queue a global task=] on the [=permission task source=] given |global| to [=/resolve=] |p| with false, and abort these steps.
1. If |flag set|'s [=has storage access flag=] is set, [=queue a global task=] on the [=permission task source=] given |global| to [=/resolve=] |p| with true, and abort these steps.
1. Let |hasAccess| be [=a new promise=].
1. [=Determine the storage access policy=] with |key|, |doc| and |hasAccess|.
1. [=Queue a global task=] on the [=permission task source=] given |global| to [=/resolve=] |p| with the result of |hasAccess|.
1. Return |p|.
ISSUE: Shouldn't step 7 be [=same site=]?
When invoked on {{Document}} |doc|, the <dfn export method for=Document><code>requestStorageAccess()</code></dfn> method must run these steps:
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess -->
<!-- https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/dom/DocumentStorageAccess.cpp#L123 -->
<!-- https://hg.mozilla.org/mozilla-central/file/tip/dom/base/Document.cpp#l15629 -->
1. Let |p| be [=a new promise=].
1. If this algorithm was invoked when |doc|'s {{Window}} object did not have [=transient activation=], [=reject=] and return |p|.
1. If |doc|'s [=Document/browsing context=] is a [=top-level browsing context=], [=/resolve=] and return |p|.
1. If |doc|'s [=Document/browsing context=]'s [=parent browsing context=] is not a [=top-level browsing context=], [=reject=] and return |p|.
1. If the [=top-level origin=] of |doc|'s [=relevant settings object=] is an [=opaque origin=], [=reject=] and return |p|. <!-- https://github.com/privacycg/storage-access/issues/40 -->
1. If |doc|'s [=Document/origin=] is [=same origin=] with the [=top-level origin=] of |doc|'s [=relevant settings object=], [=/resolve=] and return |p|.
1. If |doc|'s [=Document/origin=] is an [=opaque origin=], [=reject=] and return |p|.
1. If |doc|'s [=active sandboxing flag set=] has its [=sandbox storage access by user activation flag=] set, [=reject=] and return |p|.
1. Let |key| be the result of [=generate a partitioned storage key|generating a partitioned storage key=] from |doc|.
1. If |key| is failure, [=reject=] and return |p|.
1. Let |global| be |doc|'s [=relevant global object=].
1. Let |map| be the result of [=obtain the storage access map|obtaining the storage access map=] for |doc|.
1. Let |flag set| be the result of [=obtain a storage access flag set|obtaining the storage access flag set=] with |key| from |map|.
1. If |flag set|'s [=was expressly denied storage access flag=] is set, [=reject=] and return |p|.
1. If |flag set|'s [=has storage access flag=] is set, [=/resolve=] and return |p|.
1. Otherwise, run these steps [=in parallel=]:
1. Let |hasAccess| be [=a new promise=].
1. [=Determine the storage access policy=] with |key|, |doc| and |hasAccess|.
1. [=Queue a global task=] on the [=permission task source=] given |global| to
1. Set |flag set|'s [=has storage access flag=].
1. Resolve or reject |p| based on the result of |hasAccess|.
1. [=Save the storage access flag set=] for |key| in |map|.
1. Return |p|.
ISSUE: Shouldn't step 3.7 be [=same site=]?
ISSUE(10): Remove step 3.9 if we determine that nested <{iframe}>s should be able to request storage access.
<h4 id="ua-policy">User Agent storage access policies</h4>
Different User Agents have different policies around whether or not [=sites=] may access their [=unpartitioned data=] when they're in a [=third party context=]. User Agents check and/or modify these policies when client-side storage is accessed (see [[#storage]]) as well as when {{Document/hasStorageAccess()}} and {{Document/requestStorageAccess()}} are called.
To <dfn type="abstract-op">determine if a site has storage access</dfn> with [=partitioned storage key=] |key| and {{Document}} |doc|, run these steps:
1. Let |map| be the result of [=obtain the storage access map|obtaining the storage access map=] for |doc|.
1. Let |flag set| be the result of [=obtain a storage access flag set|obtaining the storage access flag set=] with |key| from |map|.
1. If |flag set|'s [=has storage access flag=] is set, return true.
1. Let |has storage access| (a [=boolean=]) be the result of running an [=implementation-defined=] set of steps to determine if |key|'s [=partitioned storage key/embedded site=] has access to its [=unpartitioned data=] on |key|'s [=partitioned storage key/top-level site=].
1. If |has storage access| is true, set |flag set|'s [=has storage access flag=].
1. [=Save the storage access flag set=] for |key| in |map|.
1. Return |has storage access|.
To <dfn type="abstract-op">determine the storage access policy</dfn> for [=partitioned storage key=] |key| with {{Document}} |doc| and {{Promise}} |p|, run these steps:
1. Let |map| be the result of [=obtain the storage access map|obtaining the storage access map=] for |doc|.
1. Let |flag set| be the result of [=obtain a storage access flag set|obtaining the storage access flag set=] with |key| from |map|.
1. Let |implicitly granted| and |implicitly denied| (each a [=boolean=]) be the result of running an [=implementation-defined=] set of steps to determine if |key|'s [=partitioned storage key/embedded site=]'s request for storage access on |key|'s [=partitioned storage key/top-level site=] should be granted or denied without prompting the user.
Note: These [=implementation-defined=] set of steps might result in |flag set|'s [=has storage access flag=] and [=was expressly denied storage access flag=] changing, since the User Agent could have relevant out-of-band information (e.g. a user preference that changed) that this specification is unaware of.
1. Let |global| be |doc|'s [=relevant global object=].
1. If |implicitly granted| is true, [=queue a global task=] on the [=permission task source=] given |global| to [=/resolve=] |p|, and return.
1. If |implicitly denied| is true, [=queue a global task=] on the [=permission task source=] given |global| to [=/reject=] |p|, and return.
1. Ask the user if they would like to grant |key|'s [=partitioned storage key/embedded site=] access to its [=unpartitioned data=] when it's loaded in a [=third party context=] on |key|'s [=partitioned storage key/top-level site=], and wait for an answer. Let |expressly granted| and |expressly denied| (both [=booleans=]) be the result.
Note: While |expressly granted| and |expressly denied| cannot both be true, they could both be false in User Agents which allow users to dismiss the prompt without choosing to allow or deny the request. (Such a dismissal is interpreted in this algorithm as a denial.)
1. If |expressly granted| is true, run these steps:
1. Unset |flag set|'s [=was expressly denied storage access flag=].
1. [=Save the storage access flag set=] for |key| in |map|.
1. [=Queue a global task=] on the [=permission task source=] given |global| to [=/resolve=] |p|, and return.
1. Unset |flag set|'s [=has storage access flag=].
1. If |expressly denied| is true, run these steps:
1. If |doc|'s {{Window}} object has [=transient activation=], [=consume user activation=] with it.
1. Set |flag set|'s [=was expressly denied storage access flag=].
1. [=Save the storage access flag set=] for |key| in |map|.
1. [=Queue a global task=] on the [=permission task source=] given |global| to [=/reject=] |p|.
ISSUE: [since this is UA-defined, does it make sense to follow-up separately with a user prompt?](https://github.com/privacycg/storage-access/pull/24#discussion_r408784492)
<h3 id="navigation">Changes to navigation</h3>
Before changing the [=current entry=] of a [=session history=], run the following steps:
1. Let |doc| be [=current entry=]'s {{Document}}.
1. Let |map| be the result of [=obtain the storage access map|obtaining the storage access map=] for |doc|'s [=Document/browsing context=]'s [=top-level browsing context=].
1. Let |key| be the result of [=generate a partitioned storage key|generating a partitioned storage key=] from |doc|.
1. If |key| is failure, abort these steps.
1. Let |flag set| be the result of [=obtain a storage access flag set|obtaining the storage access flag set=] with |key| from |map|.
1. Unset |flag set|'s [=has storage access flag=].
1. [=Save the storage access flag set=] for |key| in |map|.
ISSUE(3): What this section should look like ultimately hinges on
<h3 id="storage">Changes to various client-side storage mechanisms</h3>
ISSUE: Write this section. For each kind of client-side storage affected, modify them to invoke [=determine if a site has storage access=] & modify their behavior based on the result.
ISSUE(4): Should this API affect client-side storage other than cookies?
<h4 id="cookies">Cookies</h4>
ISSUE: Write this section.
<h3 id="sandboxing-storage-access">Sandboxing storage access</h3>
A [=sandboxing flag set=] has a <dfn export>sandbox storage access by user activation flag</dfn>. This flag prevents content from requesting storage access.
To the [=parse a sandboxing directive=] algorithm, add the following under step 3:
<ul>
<li>The [=sandbox storage access by user activation flag=], unless <var ignore>tokens</var> contains the <dfn export attr-value for=iframe/sandbox>allow-storage-access-by-user-activation</dfn> keyword.
</ul>
ISSUE(12): What about Feature Policy?
<h2 id="privacy">Privacy considerations</h2>
ISSUE: Write this section.
<figure id=example-prompt>
<img src=images/storage-access-prompt.png
alt="A modal dialog box which states 'Do you want to allow “video.example” to use cookies and website data while browsing “news.example”? This will allow “video.example” to track your activity.' and which has two buttons, “Don’t Allow” and “Allow”.">
<figcaption>An example prompt which could be shown to the user when a site calls `document.`{{Document/requestStorageAccess()}}.</figcaption>
</figure>
<h2 id="security">Security considerations</h2>
ISSUE: Write this section.
<h2 id="automation">Automation</h2>
For the purposes of user-agent automation and application testing, this document defines the following [=extension command=] for the [[WebDriver]] specification.
<h3 id="set-storage-access-command">Set Storage Access</h3>
<table>
<tbody>
<tr>
<th>HTTP Method</th>
<th>URI Template</th>
</tr>
<tr>
<td>POST</td>
<td>/session/{session id}/storageaccess</td>
</tr>
</tbody>
</table>
The <dfn export>Set Storage Access</dfn> [=extension command=] modifies the storage access policy for the [=current browsing context=].
The [=remote end steps=] are:
1. Let |blocked| be the result of [=getting a property=] from |parameters| named `blocked`.
1. If |blocked| is not a [=boolean=] return a [=WebDriver error=] with [=WebDriver error code=] [=invalid argument=].
1. Let |origin| be the result of [=getting a property=] from |parameters| named `origin`.
1. If |origin| is not a single U+002A ASTERISK character (*), then:
1. Let |parsedURL| be the the result of running the [=URL parser=] on |origin|.
1. If |parsedURL| is failure, then return a [=WebDriver error=] with [=WebDriver error code=] [=invalid argument=].
1. Set |origin| to |parsedURL|'s [=url/origin=].
1. If the [=current browsing context=] is not a [=top-level browsing context=] return a [=WebDriver error=] with [=WebDriver error code=] [=unsupported operation=].
1. Let |doc| be the [=current browsing context=]'s [=active document=].
1. Let |settings| be |doc|'s [=relevant settings object=].
1. Let |top-level site| be the result of [=obtain a site|obtaining a site=] from |settings|'s [=environment settings object/origin=].
1. If |origin| is a single U+002A ASTERISK character (*), then:
1. If |blocked| is `true`, then:
1. Run an [=implementation-defined=] set of steps to ensure that no site has access to its [=unpartitioned data=] when loaded in a [=third party context=] on |top-level site|.
1. Otherwise, if |blocked| is `false`, then:
1. Run an [=implementation-defined=] set of steps to ensure that any site has access to its [=unpartitioned data=] when loaded in a [=third party context=] on |top-level site|.
1. Otherwise:
1. Let |embedded site| be the result of [=obtain a site|obtaining a site=] from |origin|.
1. If |embedded site| is [=same site=] with |top-level site| return a [=WebDriver error=] with [=WebDriver error code=] [=unsupported operation=].
1. If |blocked| is `true`, then:
1. Run an [=implementation-defined=] set of steps to ensure that |embedded site| does not have access to its [=unpartitioned data=] when loaded in a [=third party context=] on |top-level site|.
1. Otherwise, if |blocked| is `false`, then:
1. Run an [=implementation-defined=] set of steps to ensure that |embedded site| has access to its [=unpartitioned data=] when loaded in a [=third party context=] on |top-level site|.
1. If the above [=implementation-defined=] step of steps resulted in failure, return a [=WebDriver error=] with [=WebDriver error code=] [=unknown error=].
1. Return [=success=] with data `null`.
<h2 id="acknowledgements" class="no-num">Acknowledgements</h2>
Many thanks to
Anne van Kesteren,
Ben Kelly,
Brad Girardeau,
Brad Hill,
Brady Eidson,
Brandon Maslen,
Chris Mills,
Dave Longley,
Domenic Denicola,
Ehsan Akhgari,
Geoffrey Garen,
Jack Frankland,
James Coleman,
James Hartig,
Jeffrey Yasskin,
Kushal Dave,
Luís Rudge,
Maciej Stachowiak,
Matias Woloski,
Mike O'Neill,
Mike West,
Pete Snyder,
Rob Stone,
Stefan Leyhane,
Steven Englehardt,
Theresa O'Connor,
Travis Leithead,
Yan Zhu,
Zach Edwards,
and everyone who commented on [whatwg/html#3338](https://github.com/whatwg/html/issues/3338), [privacycg/proposals#2](https://github.com/privacycg/proposals/issues/2), and [privacycg/storage-access/issues](https://github.com/privacycg/storage-access/issues)
for their feedback on this proposal.
Thanks to the [WebKit Open Source Project](https://webkit.org/) for allowing us to use the [Storage Access API Prompt](#example-prompt) image, which was [originally published on webkit.org](https://webkit.org/blog/8311/intelligent-tracking-prevention-2-0/).