forked from WICG/shared-storage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathspec.bs
1520 lines (1206 loc) · 103 KB
/
spec.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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<pre class='metadata'>
Title: Shared Storage API
Shortname: sharedStorage
Level: 1
Status: CG-DRAFT
Group: WICG
URL: https://github.com/WICG/shared-storage
Editor: Camillia Smith Barnes, Google https://google.com, [email protected]
Markup Shorthands: markdown yes
Abstract: Shared Storage is a storage API that is intentionally not partitioned by top-frame site (though still partitioned by context origin of course!). To limit cross-site reidentification of users, data in Shared Storage may only be read in a restricted environment that has carefully constructed output gates.
</pre>
<pre class=link-defaults>
spec:infra;
type:dfn;
text:user agent
for:/; text:string
spec:webidl;
type:interface;
text:double
spec:html;
type:dfn;
for:realm; text:global object
for:WorkerGlobalScope; text:module map
for:navigable; text:top-level traversable
</pre>
<pre class="anchors">
spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
type: dfn
text: worklets; url: worklets.html#worklets
text: environment; url: webappapis.html#environment
text: obtaining a worklet agent; url: webappapis.html#obtain-a-worklet-agent
text: top-frame; url: webappapis.html#top-level-traversable
text: beginning navigation; url: webappapis.html#beginning-navigation
text: ending navigation; url: webappapis.html#ending-navigation
text: get the top-level traversable; url: webappapis.html#nav-top
text: boolean attributes; url: common-microsyntaxes.html#boolean-attributes
text: content attributes; url: dom.html#concept-element-attributes
text: update the image data; url: images.html#update-the-image-data
text: create navigation params by fetching; url: browsing-the-web.html#create-navigation-params-by-fetchin
spec: url; urlPrefix: https://url.spec.whatwg.org/
type: dfn
text: URL; for: /; url: concept-url
spec: dom; urlPrefix: https://dom.spec.whatwg.org/
type: dfn
text: origin; for: document; url: concept-document-origin
spec: infra; urlPrefix: https://infra.spec.whatwg.org
type: dfn
text: empty; for: map; url: map-is-empty
text: ASCII; url: ascii-code-point
spec: webidl; urlPrefix: https://webidl.spec.whatwg.org
type: dfn
text: Web IDL Standard; url: introduction
text: async iterator; url: idl-async-iterable
text: promise; url: idl-promise
text: promise rejected; url: a-promise-rejected-with
spec: storage; urlPrefix: https://storage.spec.whatwg.org/
type: dfn
text: storage model; url: model
text: storage type; url: storage-type
text: storage identifier; url: storage-identifier
text: storage shed; url: storage-shed
text: storage shelf; url: storage-shelf
text: storage bucket; url: storage-bucket
text: storage bottle; url: storage-bottle
text: quota; for: storage bottle; url: storage-bottle-quota
text: register; for: storage endpoint; url: registered-storage-endpoints
text: quota; for: storage endpoint; url: storage-endpoint-quota
text: bucket map; url: bucket-map
text: bottle map; url: bottle-map
text: storage proxy map; url: storage-proxy-map
text: backing map; url: storage-proxy-map-backing-map
text: proxy map reference set; url: storage-bottle-proxy-map-reference-set
spec: beacon; urlPrefix: https://w3c.github.io/beacon/
type: dfn
text: beacon; url: beacon
spec: ecma; urlPrefix: https://tc39.es/ecma262/
type: dfn
text: call; url: sec-call
text: current realm; url: current-realm
text: casting; url: sec-touint32
text: GetMethod(); url: sec-getmethod
text: [[GetPrototypeOf]](); for: object; url: sec-ordinary-object-internal-methods-and-internal-slots-getprototypeof
text: IsConstructor(); url: sec-isconstructor
spec: storage-partitioning; urlPrefix: https://privacycg.github.io/storage-partitioning/
type: dfn
text: client-side storage partitioning
spec: fetch; urlPrefix: https://fetch.spec.whatwg.org/
type: dfn
text: http network or cache fetch; url: concept-http-network-or-cache-fetch
text: request-constructor; url: dom-request
spec: permissions-policy; urlPrefix: https://www.w3.org/TR/permissions-policy/
type: dfn
text: Is feature enabled in document for origin?; url: algo-is-feature-enabled
spec: attestation; urlPrefix: https://github.com/privacysandbox/attestation
type: dfn
text: enrolled
spec: private-aggregation-api; urlPrefix: https://github.com/patcg-individual-drafts/private-aggregation-api/blob/main/README.md
type: dfn
text: private aggregation
spec: fenced-frame; urlPrefix: https://wicg.github.io/fenced-frame/
type: dfn
text: fenced frame; url: the-fencedframe-element
text: url; for: FencedFrameConfig; url: dom-fencedframeconfig-url
text: fence.reportEvent(); url: dom-fence-reportevent
text: FenceEvent; url: dictdef-fenceevent
text: destination; for: FenceEvent; url: dom-fenceevent-destination
text: eventData; for: FenceEvent; url: dom-fenceevent-eventdata
text: eventType; for: FenceEvent; url: dom-fenceevent-eventtype
type: interface
text: FencedFrameConfig; url: fencedframeconfig
spec: rfc8941; urlPrefix: https://httpwg.org/specs/rfc8941.html
type: dfn
text: structured header; url: specify
text: list; for: structured header; url: list
text: parameters; for: structured header; url: param
text: item; for: structured header; url: item
text: string; for: structured header; url: string
text: token; for: structured header; url: token
text: byte sequence; for: structured header; url: binary
text: boolean; for: structured header; url: boolean
text: inner list; for: structured header; url: inner-list
text: bare item; for: structured header; url: item
spec: wikipedia-entropy; urlPrefix: https://en.wikipedia.org/wiki/Entropy_(information_theory)
type: dfn
text: bits of entropy
text: entropy bits
spec: shared-storage-explainer; urlPrefix: https://github.com/WICG/shared-storage
type:dfn
text: legitimate use cases; url: example-scenarios
</pre>
<style>
/* adapted from .XXX at https://resources.whatwg.org/standard.css */
.todo {
color: #D50606;
background: white;
border: solid #D50606;
}
span.todo {
padding-top: 0;
padding-bottom: 0;
}
.todo::before { content: 'TODO: '; }
span.todo::before {
left: 0;
top: -0.25em;
}
</style>
Introduction {#intro}
=====================
<em>This section is not normative.</em>
In order to prevent cross-site user tracking, browsers are partitioning all forms of storage by [=top-frame=] site; see [=Client-Side Storage Partitioning=]. But, there are many [=legitimate use cases=] currently relying on unpartitioned storage.
This document introduces a new storage API that is intentionally not partitioned by [=top-frame=] site (though still partitioned by context origin), in order to serve a number of the use cases needing unpartitioned storage. To limit cross-site reidentification of users, data in Shared Storage may only be read in a restricted environment, called a worklet, and any output from the worklet is in the form of a [=fenced frame=] or a [=private aggregation|private aggregation report=]. Over time, there may be additional ouput gates included in the standard.
<div class="example">
`a.example` randomly assigns users to groups in a way that is consistent cross-site.
Inside an `a.example` iframe:
<pre class="lang-js">
function generateSeed() { … }
await window.sharedStorage.worklet.addModule('experiment.js');
// Only write a cross-site seed to a.example's storage if there isn't one yet.
window.sharedStorage.set('seed', generateSeed(), { ignoreIfPresent: true });
let fencedFrameConfig = await window.sharedStorage.selectURL(
'select-url-for-experiment',
[
{url: "blob:https://a.example/123…", reportingMetadata: {"click": "https://report.example/1..."}},
{url: "blob:https://b.example/abc…", reportingMetadata: {"click": "https://report.example/a..."}},
{url: "blob:https://c.example/789…"}
],
{ data: { name: 'experimentA' } }
);
// Assumes that the fenced frame 'my-fenced-frame' has already been attached.
document.getElementById('my-fenced-frame').config = fencedFrameConfig;
</pre>
inside the `experiment.js` worklet script:
<pre class="lang-js">
class SelectURLOperation {
hash(experimentName, seed) { … }
async run(urls, data) {
const seed = await this.sharedStorage.get('seed');
return hash(data.name, seed) % urls.length;
}
}
register('select-url-for-experiment', SelectURLOperation);
</pre>
</div>
The {{SharedStorageWorklet}} Interface {#worklet}
=================================================
The {{SharedStorageWorklet}} object allows developers to supply [=module scripts=] to process [=Shared Storage=] data and then output the result through one or more of the output gates. Currently there are two output gates, the [=private aggregation=] output gate and the {{WindowSharedStorage/selectURL()|URL-selection}} output gate.
<xmp class='idl'>
[Exposed=(Window)]
interface SharedStorageWorklet : Worklet {
};
</xmp>
Each {{SharedStorageWorklet}} has an associated boolean <dfn>addModule initiated</dfn>, initialized to false.
Because adding multiple [=module scripts=] via {{Worklet/addModule()}} for the same {{SharedStorageWorklet}} would give the caller the ability to store data from [=Shared Storage=] in global variables defined in the [=module scripts=] and then exfiltrate the data through later call(s) to {{Worklet/addModule()}}, each {{SharedStorageWorklet}} can only call {{Worklet/addModule()}} once. The [=addModule initiated=] boolean makes it possible to enforce this restriction.
When {{Worklet/addModule()}} is called for a worklet, it will run [=check if addModule is allowed and update status=], and if the result is false, abort the remaining steps in the {{Worklet/addModule()}} call, as detailed in the [[#worklet-monkey-patch]].
<div algorithm>
To <dfn>check if user preference setting allows access to shared storage</dfn> from an [=environment settings object=] |environment|, run the following step:
1. Using values available in |environment| as needed, perform an [=implementation-defined=] algorithm to return either true or false.
</div>
<div algorithm>
To <dfn>determine whether shared storage is allowed</dfn>, given an [=environment settings object=] |environment|, run these steps:
1. If |environment| is not a [=secure context=], then return false.
1. Let |origin| be |environment|'s [=url/origin=].
1. If |origin| is an [=opaque origin=], then return false.
1. Let |globalObject| be the [=current realm=]'s [=global object=].
1. [=Assert=] that |globalObject| is a {{Window}} or a {{SharedStorageWorkletGlobalScope}}.
1. If |globalObject| is a {{Window}} and |globalObject|'s [=associated document=] is not [=allowed to use=] the "[=PermissionsPolicy/shared-storage=]" feature, return false.
1. If the result of running [=obtaining a site|obtain a site=] with |origin| is not [=enrolled=], then return false.
1. If the result of running [=check if user preference setting allows access to shared storage=] from |environment| is false, then return false.
1. Return true.
</div>
<div algorithm>
To <dfn>check if addModule is allowed and update status</dfn> for a {{SharedStorageWorklet}} |worklet|, run the following steps:
1. If the result of running [=determine whether shared storage is allowed=] on the [=relevant settings object=] of [=this=] is false, return false.
1. If |worklet|'s [=addModule initiated=] is true, return false.
1. Set |worklet|'s [=addModule initiated=] to true.
1. Return true.
</div>
Moreover, each {{SharedStorageWorklet}}'s [=global scopes|list of global scopes=], initially empty, can contain at most one instance of its [=worklet global scope type=], the {{SharedStorageWorkletGlobalScope}}.
## Monkey Patch for [=Worklets=] ## {#worklet-monkey-patch}
This specification will make some modifications to the [=Worklet=] standard to accommodate the needs of Shared Storage.
In particular, the {{Worklet/addModule()}} method steps for {{Worklet}} will need to be prepended with the following step:
0. If {{Worklet}} has an associated boolean [=addModule initiated=], and the result of running [=check if addModule is allowed and update status=] on {{Worklet}} is false, return a [=promise rejected=] with a {{TypeError}}.
And the penultimate step (i.e. the final indented step), currently "If |pendingTasks| is 0, then resolve |promise|.", should be updated to:
2. If |pendingTasks| is 0, perform the following steps:
1. If |workletGlobalScope| has an associated boolean [=addModule success=], set |workletGlobalScope|'s [=addModule success=] to true.
2. Resolve |promise|.
<span class=todo>Add additional monkey patch pieces for out-of-process worklets.</span>
## The {{SharedStorageWorkletGlobalScope}} ## {#global-scope}
The {{SharedStorageWorklet}}'s [=worklet global scope type=] is {{SharedStorageWorkletGlobalScope}}.
<xmp class='idl'>
[Exposed=SharedStorageWorklet, Global=SharedStorageWorklet]
interface SharedStorageWorkletGlobalScope : WorkletGlobalScope {
undefined register(DOMString name,
SharedStorageOperationConstructor operationCtor);
readonly attribute WorkletSharedStorage sharedStorage;
};
callback SharedStorageOperationConstructor =
SharedStorageOperation(optional SharedStorageRunOperationMethodOptions options);
[Exposed=SharedStorageWorklet]
interface SharedStorageOperation {
};
dictionary SharedStorageRunOperationMethodOptions {
object data;
boolean resolveToConfig = false;
boolean keepAlive = false;
};
</xmp>
Each {{SharedStorageWorkletGlobalScope}} has an associated [=environment settings object=] <dfn for=SharedStorageWorkletGlobalScope>outside settings</dfn>, which is the associated {{SharedStorageWorklet}}'s [=relevant settings object=].
Each {{SharedStorageWorkletGlobalScope}} has an associated [=/boolean=] <dfn for=SharedStorageWorkletGlobalScope>addModule success</dfn>, which is initialized to false.
The {{SharedStorageWorkletGlobalScope}}'s [=module map=]'s [=module scripts=] should each define and {{register}} one or more {{SharedStorageOperation}}s.
Each {{SharedStorageWorkletGlobalScope}} also has an associated <dfn for=SharedStorageWorkletGlobalScope>operation map</dfn>, which is a [=map=], initially empty, of [=strings=] (denoting operation names) to [=functions=].
Currently each {{SharedStorageOperation}} registered via {{SharedStorageWorkletGlobalScope/register()}} must be one of the following two types:
* {{SharedStorageRunOperation}}
* {{SharedStorageSelectURLOperation}}
The {{SharedStorageRunOperation}} is designed to work with output gates that do not need a return value, like the [=private aggregation=] service. A {{SharedStorageRunOperation}} performs an async operation and returns a promise that resolves to undefined.
A {{SharedStorageSelectURLOperation}} is an {{SharedStorageOperation}} that takes in a [=/list=] of {{SharedStorageUrlWithMetadata}}s (i.e. [=dictionaries=] containing [=strings=] representing [=/URLs=] each wrapped with optional metadata), performs an async operation, and then returns a promise to a {{long}} integer index specifying which of these [=/URLs=] should be selected.
<xmp class='idl'>
[Exposed=SharedStorageWorklet]
interface SharedStorageRunOperation : SharedStorageOperation {
Promise<undefined> run(object data);
};
[Exposed=SharedStorageWorklet]
interface SharedStorageSelectURLOperation : SharedStorageOperation {
Promise<long> run(object data,
FrozenArray<SharedStorageUrlWithMetadata> urls);
};
</xmp>
Each {{SharedStorageWorkletGlobalScope}} also has an associated {{WorkletSharedStorage}} instance, with the [=SharedStorageWorkletGlobalScope/sharedStorage getter=] algorithm as described below.
## {{SharedStorageWorkletGlobalScope}} algorithms ## {#scope-algo}
<div algorithm>
The <dfn method for="SharedStorageWorkletGlobalScope">register(|name|, |operationCtor|)</dfn> method steps are:
1. If |name| is missing or empty, throw a {{TypeError}}.
1. Let |operationMap| be this {{SharedStorageWorkletGlobalScope}}'s [=SharedStorageWorkletGlobalScope/operation map=].
1. If |operationMap| [=map/contains=] an [=map/entry=] with [=map/key=] |name|, throw a {{TypeError}}.
1. If |operationCtor| is missing, throw a {{TypeError}}.
1. If the result of running [=IsConstructor()=] with |operationCtor| is false, throw a {{TypeError}}.
1. Let |prototype| be the result of running |operationCtor|'s [=object/[[GetPrototypeOf]]()=] method.
1. If |prototype| is not an [=object=], throw a {{TypeError}}.
1. Let |run| be the result of running [=GetMethod()=] with |prototype| and "`run`".
1. If |run| is undefined, throw a {{TypeError}}.
1. [=map/Set=] the value of |operationMap|[|name|] to |run|.
</div>
<div algorithm>
The <dfn for="SharedStorageWorkletGlobalScope">{{SharedStorageWorkletGlobalScope/sharedStorage}} getter</dfn> steps are:
1. If |this|'s [=addModule success=] is true, return |this|'s {{SharedStorageWorkletGlobalScope/sharedStorage}}.
1. Otherwise, throw a {{TypeError}}.
</div>
<div algorithm>
To <dfn for="SharedStorageWorkletGlobalScope">check whether addModule is finished</dfn>, the step is:
1. Return the value of [=addModule success=].
</div>
Shared Storage's Backend {#backend}
===================================
The Shared Storage API will integrate into the [=Storage Model|Storage API=] as below, via [=storage endpoint/registering=] a new [=storage endpoint=].
## Monkey Patch for the [=Storage Model=] ## {#storage-monkey-patch}
This standard will add a new [=storage type=] "`shared`" to the [=Storage Model=].
A [=user agent=] holds a [=shared storage shed=] for [=storage endpoints=] of [=storage type|type=] "`shared`".
This standard will also [=storage endpoint/register=] a [=storage endpoint=] of [=storage type|type=] "`shared`" with [=storage identifier=] "`sharedStorage`" and [=storage endpoint/quota=] `5`<sup>`4`</sup> `*` `2`<sup>`16`</sup> bytes (i.e. 39.0625 mebibytes).
<span class=todo>This [=storage endpoint/quota=] is calculated from the current implementation. Consider bringing the current implementation in line with the spec for [=storage endpoints=] "`localStorage`" and "`sessionStorage`", i.e. `5 * 2`<sup>`20`</sup> bytes. For example, decreasing the per-origin entry limit from 10,000 to 1,280 would achieve this.</span>
A <dfn>shared storage shed</dfn> is a [=map=] of [=/origins=] to [=storage shelf|storage shelves=]. It is initially empty.
Note: Unlike [=storage sheds=], whose keys are [=storage keys=], [=shared storage sheds=] use [=/origins=] as keys directly. [=Shared storage=] will be intentionally excluded from [=client-side storage partitioning=].
For each [=storage shelf=] in a [=shared storage shed=], the [=storage shelf=]'s [=bucket map=] currently has only a single key of "`default`".
A [=user agent=]'s [=shared storage shed=] holds all <dfn>shared storage</dfn> data.
<div algorithm>
To <dfn>obtain a shared storage shelf</dfn>, given a [=shared storage shed=] |shed| and an [=environment settings object=] |environment|, run these steps:
1. If the result of running [=determine whether shared storage is allowed=] on |environment| is false, then return failure.
1. Let |origin| be |environment|'s [=url/origin=].
1. If |shed|[origin] does not exist, then set |shed|[origin] to the result of running [=create a shared storage shelf=] with [=storage type|type=] "`shared`".
1. Return |shed|[|origin|].
</div>
<div algorithm>
To <dfn>create a shared storage shelf</dfn>, run these steps:
1. Let |shelf| be a new [=storage shelf=].
1. Set |shelf|'s [=bucket map=]["`default`"] to the result of running [=create a shared storage bucket=].
1. Return |shelf|.
</div>
A <dfn>shared storage bucket</dfn> is a [=storage bucket=] in one of a [=shared storage shed=]'s [=storage shelf|shelves=].
<div algorithm>
To <dfn>create a shared storage bucket</dfn>, run these steps:
1. Let |endpoint| be the [=storage endpoint=] with [=storage identifier=] "`sharedStorage`".
1. Let |bucket| be a new [=shared storage bucket=].
1. Set |bucket|'s [=bottle map=]["`sharedStorage`"] to a new [=storage bottle=] whose [=storage bottle/quota=] is |endpoint|'s [=storage endpoint/quota=].
1. Return |bucket|.
</div>
Note: Currently, a [=shared storage bucket=]'s [=bottle map=] has [=map/size=] `1`, since there is only one [=storage endpoint=] [=storage endpoint/registered=] with [=storage type|type=] "`shared`".
<div algorithm>
To <dfn>obtain a shared storage bottle map</dfn>, given an [=environment settings object=] |environment|, run these steps:
1. Let |shed| be the [=user agent=]'s [=shared storage shed=].
1. Let |shelf| be the result of running [=obtain a shared storage shelf=] with |shed| and |environment|.
1. If |shelf| is failure, then return failure.
1. Let |bucket| be |shelf|'s [=bucket map=]["`default`"].
1. Let |bottle| be |bucket|'s [=bottle map=]["`sharedStorage`"].
1. Let |proxyMap| be a new [=storage proxy map=] whose [=backing map=] is |bottle|'s [=map=].
1. [=set/Append=] |proxyMap| to |bottle|'s [=proxy map reference set=].
1. Return |proxyMap|.
</div>
## The [=Shared Storage Database=] ## {#database}
A [=/browsing context=] has an associated <dfn>shared storage database</dfn>, which provides methods to [=shared storage database/store an entry in the database|store=], [=shared storage database/retrieve an entry from the database|retrieve=], [=shared storage database/delete an entry from the database|delete=], [=shared storage database/clear all entries in the database|clear=], and [=shared storage database/purge expired entries from the database|purge expired=] data, and additional methods as below. The data in the [=shared storage database|database=] take the form of [=shared storage database/entry|entries=].
Each [=shared storage database=] has a <dfn for="shared storage database">shared storage database queue</dfn>, which is the result of [=start a new parallel queue|starting a new parallel queue=]. This [=shared storage database queue|queue=] is used to run each of the [=shared storage database=]'s methods when calls to them are initiated from that [=/browsing context=].
Each <dfn for="shared storage database">entry</dfn> consists of a [=entry/key=] and a [=entry/value struct=].
An [=shared storage database/entry=]'s <dfn for=entry>key</dfn> is a [=string=].
[=User agents=] may specify the <dfn for=key>maximum length</dfn> of a [=entry/key=].
Since [=entry/keys=] are used to organize and efficiently retrieve [=shared storage database/entry|entries=], [=entry/keys=] must appear at most once in any given [=shared storage database=].
An [=shared storage database/entry=]'s <dfn for=entry>value struct</dfn> is a [=struct=] composed of [=string=] <dfn for="value struct">value</dfn> and {{DOMHighResTimeStamp}} <dfn for="value struct">last updated</dfn> (from the [=Unix Epoch=]).
[=User agents=] may specify the <dfn for=value>maximum length</dfn> of a [=value struct/value=].
[=User agents=] may specify a <dfn>default entry lifetime</dfn>, the default [=duration=] between when an [=shared storage database/entry=] is [=shared storage database/store an entry in the database|stored=] and when it expires. If the [=user agent=] specifies a [=default entry lifetime=], then it should have a timer periodically [=shared storage database/purge expired entries from the database=].
## The [=Shared Storage Database|Database=] Algorithms ## {#database-algorithms}
<div algorithm>
To <dfn for="shared storage database">store an entry in the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue|, a [=storage proxy map=] |databaseMap|, an [=environment settings object=] |environment|, a [=entry/key=] |key|, and a [=value struct/value=] |value|, run the following steps on |queue|:
1. Let |valueStruct| be a new [=entry/value struct=].
1. Set |valueStruct|'s [=value struct/value=] to |value|.
1. Let |currentTime| be the |environment|'s [=environment settings object/current wall time=].
1. Set |valueStruct|'s [=value struct/last updated=] to |currentTime|.
1. [=map/Set=] |databaseMap|[|key|] to |valueStruct|.
1. If this throws an exception, catch it and return false.
Note: Errors with [=storage proxy map=] |databaseMap|'s methods are possible depending on its implementation.
1. Otherwise, return true.
</div>
<div algorithm>
To <dfn for="shared storage database">retrieve an entry from the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue|, a [=storage proxy map=] |databaseMap|, an [=environment settings object=] |environment|, and a [=entry/key=] |key|, run the following steps on |queue|:
1. If |databaseMap| does not [=map/contain=] |key|, return undefined.
1. Let |valueStruct| be the result of running [=map/Get=] on |databaseMap| with |key|.
1. If this throws an exception, catch it and return failure.
Note: Errors with [=storage proxy map=] |databaseMap|'s methods are possible depending on its implementation.
1. If the result of running [=shared storage database/determine whether an entry is expired=] with |environment| and |valueStruct| is true, return undefined.
1. Return |valueStruct|'s [=value struct/value=].
</div>
<div algorithm>
To <dfn for="shared storage database">delete an entry from the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue|, a [=storage proxy map=] |databaseMap|, and a [=entry/key=] |key|, run the following steps on |queue|:
1. [=map/Remove=] |databaseMap|[|key|].
1. If this throws an exception, catch it and return false.
Note: Errors with [=storage proxy map=] |databaseMap|'s methods are possible depending on its implementation.
1. Return true.
</div>
<div algorithm>
To <dfn for="shared storage database">clear all entries in the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue| and a [=storage proxy map=] |databaseMap|, run the following steps on |queue|:
1. Run [=map/Clear=] on |databaseMap|.
1. If this throws an exception, catch it and return false.
Note: Errors with [=storage proxy map=] |databaseMap|'s methods are possible depending on its implementation.
1. Return true.
</div>
<div algorithm>
To <dfn for="shared storage database">retrieve all entries from the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue| and a [=storage proxy map=] |databaseMap|, run the following steps on |queue|:
1. Let |values| be the result of running [=map/getting the values=] on |databaseMap|.
1. If this throws an exception, catch it and return failure.
Note: Errors with [=storage proxy map=] |databaseMap|'s methods are possible depending on its implementation.
1. Return |values|.
</div>
<div algorithm>
To <dfn for="shared storage database">count entries in the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue| and a [=storage proxy map=] |databaseMap|, run the following steps on |queue|:
1. Let |size| be |databaseMap|'s [=map/size=].
1. If this throws an exception, catch it and return failure.
Note: Errors with [=storage proxy map=] |databaseMap|'s members are possible depending on its implementation.
1. Return |size|.
</div>
<div algorithm>
To <dfn for="shared storage database">purge expired entries from the database</dfn>, given a [=shared storage database/shared storage database queue=] |queue|, a [=storage proxy map=] |databaseMap|, and an [=environment settings object=] |environment|, run the following steps on |queue|:
1. [=map/iterate|For each=] [=entry/key=] |key| in |databaseMap|:
1. Let |valueStruct| be the result of running [=map/Get=] on |databaseMap| with |key|.
1. If this throws an exception, catch it and return false.
1. If the result of running [=shared storage database/determine whether an entry is expired=] with |environment| and |valueStruct| is true, [=map/Remove=] |databaseMap|[|key|].
1. If this throws an exception, catch it and return false.
1. Return true.
</div>
<div algorithm>
To <dfn for="shared storage database">determine whether an entry is expired</dfn>, given an [=environment settings object=] |environment| and a [=entry/value struct=] |valueStruct|, run the following steps:
1. Let |lastUpdated| be |valueStruct|'s [=value struct/last updated=].
1. Let |lifetime| be [=user agent=]'s [=default entry lifetime=].
1. Let |expiration| be the sum of |lastUpdated| and |lifetime|.
1. Let |currentTime| be the |environment|'s [=environment settings object/current wall time=].
1. If |expiration| is less than or equal to |currentTime|, return true.
1. Otherwise, return false.
</div>
The {{SharedStorage}} Interface {#shared-storage-interface}
==========================================================
The {{SharedStorage}} interface is the base for derived interfaces {{WindowSharedStorage}} and {{WorkletSharedStorage}}, which are exposed to the {{Window}} and the {{SharedStorageWorklet}}, respectively.
Methods that allow the setting and/or deleting of data are exposed to both the {{Window}} and the {{SharedStorageWorklet}} and hence are declared in the base {{SharedStorage}} interface, although their implementations may vary depending on their [=environment=]. This makes it possible to modify the data in Shared Storage from multiple contexts.
Meanwhile, methods for running {{SharedStorageOperation}}s, along with the {{WindowSharedStorage/worklet}} attribute which is used to call {{Worklet/addModule()}}, are declared in {{WindowSharedStorage}} and exposed to the {{Window}} only, as these are the means by which the {{Window}} interacts with the {{SharedStorageWorklet}}.
On the other hand, methods for getting data from the [=shared storage database=] are declared in {{WorkletSharedStorage}} and exposed to the {{SharedStorageWorklet}} only, in order to carefully control the flow of data read from the [=shared storage database|database=].
<xmp class='idl'>
[Exposed=(Window,SharedStorageWorklet)]
interface SharedStorage {
Promise<any> set(DOMString key,
DOMString value,
optional SharedStorageSetMethodOptions options = {});
Promise<any> append(DOMString key,
DOMString value);
Promise<any> delete(DOMString key);
Promise<any> clear();
};
dictionary SharedStorageSetMethodOptions {
boolean ignoreIfPresent = false;
};
</xmp>
## The {{WindowSharedStorage}} interface ## {#window}
The {{WindowSharedStorage}} interface is as follows.
<xmp class='idl'>
typedef (USVString or FencedFrameConfig) SharedStorageResponse;
[Exposed=(Window)]
interface WindowSharedStorage : SharedStorage {
Promise<any> run(DOMString name,
optional SharedStorageRunOperationMethodOptions options = {});
Promise<SharedStorageResponse> selectURL(DOMString name,
FrozenArray<SharedStorageUrlWithMetadata> urls,
optional SharedStorageRunOperationMethodOptions options = {});
readonly attribute SharedStorageWorklet worklet;
};
</xmp>
### Window Setter/Deleter Methods ### {#window-setter}
<div algorithm>
The <dfn method for="WindowSharedStorage">set(|key|, |value|, |options|)</dfn> method step is:
1. Return the result of running [=WindowSharedStorage/set a key-value pair=] on {{WindowSharedStorage}}, |key|, |value|, and |options|.
</div>
<div algorithm>
The <dfn method for="WindowSharedStorage">append(|key|, |value|)</dfn> method step is:
1. Return the result of running [=WindowSharedStorage/append a key-value pair=] on {{WindowSharedStorage}}, |key|, and |value|.
</div>
<div algorithm>
The <dfn method for="WindowSharedStorage">delete(|key|)</dfn> method step is:
1. Return the result of running [=WindowSharedStorage/delete a key=] on {{WindowSharedStorage}} and |key|.
</div>
<div algorithm>
The <dfn method for="WindowSharedStorage">clear()</dfn> method step is:
1. Return the result of running [=WindowSharedStorage/clear all keys=] on {{WindowSharedStorage}}.
</div>
<div algorithm>
To <dfn for="WindowSharedStorage">set a key-value pair</dfn>, given {{WindowSharedStorage}} |sharedStorage|, [=string=] |key|, [=string=] |value|, and {{SharedStorageSetMethodOptions}} |options|, perform the following steps:
1. Let |promise| be a new [=promise=].
1. If |key|'s [=string/length=] exceeds the [=key/maximum length=], return a [=promise rejected=] with a {{TypeError}}.
1. If |value|'s [=string/length=] exceeds the [=value/maximum length=], return a [=promise rejected=] with a {{TypeError}}.
1. Let |context| be |sharedStorage|'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. Let |databaseMap| be the result of running [=obtain a shared storage bottle map=] for |environment|.
1. If |databaseMap| is failure, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |queue| be |context|'s associated [=shared storage database|database=]'s [=shared storage database/shared storage database queue=].
1. Let |realm| be the [=current realm=].
1. [=Enqueue the following steps=] on |queue|:
1. If |options|["`ignoreIfPresent`"] is true and the result of running [=shared storage database/retrieve an entry from the database=] with |queue|, |databaseMap|, |environment|, and |key| is not undefined:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Abort these steps.
1. Run [=shared storage database/store an entry in the database=] with |queue|, |databaseMap|, |environment|, |key|, and |value|.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Return |promise|.
</div>
<div algorithm>
To <dfn for="WindowSharedStorage">append a key-value pair</dfn>, given {{WindowSharedStorage}} |sharedStorage|, [=string=] |key|, and [=string=] |value|, perform the following steps:
1. Let |promise| be a new [=promise=].
1. If |key|'s [=string/length=] exceeds the [=key/maximum length=], return a [=promise rejected=] with a {{TypeError}}.
1. If |value|'s [=string/length=] exceeds the [=value/maximum length=], return a [=promise rejected=] with a {{TypeError}}.
1. Let |context| be |sharedStorage|'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. Let |databaseMap| be the result of running [=obtain a shared storage bottle map=] for |environment|.
1. If |databaseMap| is failure, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |queue| be |context|'s associated [=shared storage database|database=]'s [=shared storage database/shared storage database queue=].
1. Let |realm| be the [=current realm=].
1. [=Enqueue the following steps=] on |queue|:
1. Let |currentValue| be the result of running [=shared storage database/retrieve an entry from the database=] with |queue|, |databaseMap|, |environment|, and |key|.
1. If |currentValue| is failure:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Abort these steps.
1. If |currentValue| is not undefined:
1. Let |list| be a new [=/list=].
1. [=list/Append=] |currentValue| to |list|.
1. [=list/Append=] |value| to |list|.
1. Set |value| to the result of running [=string/concatenate=] on |list|.
1. Run [=shared storage database/store an entry in the database=] with |queue|, |databaseMap|, |environment|, |key|, and |value|.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Return |promise|.
</div>
<div algorithm>
To <dfn for="WindowSharedStorage">delete a key</dfn>, given {{WindowSharedStorage}} |sharedStorage| and [=string=] |key|, perform the following steps:
1. Let |promise| be a new [=promise=].
1. If |key|'s [=string/length=] exceeds the [=key/maximum length=], return a [=promise rejected=] with a {{TypeError}}.
1. Let |context| be |sharedStorage|'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. Let |databaseMap| be the result of running [=obtain a shared storage bottle map=] for |environment|.
1. If |databaseMap| is failure, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |queue| be |context|'s associated [=shared storage database|database=]'s [=shared storage database/shared storage database queue=].
1. Let |realm| be the [=current realm=].
1. [=Enqueue the following steps=] on |queue|:
1. Run [=shared storage database/delete an entry from the database=] with |queue|, |environment|, and |key|.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Return |promise|.
</div>
<div algorithm>
To <dfn for="WindowSharedStorage">clear all keys</dfn>, given {{WindowSharedStorage}} |sharedStorage|, perform the following steps:
1. Let |promise| be a new [=promise=].
1. Let |context| be |sharedStorage|'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. Let |databaseMap| be the result of running [=obtain a shared storage bottle map=] for |environment|.
1. If |databaseMap| is failure, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |queue| be |context|'s associated [=shared storage database|database=]'s [=shared storage database/shared storage database queue=].
1. Let |realm| be the [=current realm=].
1. [=Enqueue the following steps=] on |queue|:
1. Run [=shared storage database/clear all entries in the database=] with |queue| and |environment|.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. Return |promise|.
</div>
### {{SharedStorageUrlWithMetadata}} and Reporting ### {#reporting}
A {{SharedStorageUrlWithMetadata}} {{/object}} is a [=dictionary=] containing a [=string=] representing a [=/URL=] and, optionally, a {{reportingMetadata}} {{/object}}.
<xmp class='idl'>
dictionary SharedStorageUrlWithMetadata {
required USVString url;
object reportingMetadata;
};
</xmp>
If a {{SharedStorageUrlWithMetadata}} {{/object}} contains a non-[=map/empty=] {{SharedStorageUrlWithMetadata/reportingMetadata}} {{/object}} in the form of a [=dictionary=] whose [=map/keys=] are [=FenceEvent/eventTypes=] and whose [=map/values=] are [=strings=] that parse to valid [=/URLs=], then these [=FenceEvent/eventType=]-[=/URL=] pairs will be [=register reporting metadata|registered=] for later access within any [=fenced frame=] that loads the {{SharedStorageResponse}} resulting from this {{WindowSharedStorage/selectURL()}} call.
Inside a [=fenced frame=] with [=FenceEvent/eventType=]-[=/URL=] pairs that have been [=register reporting metadata|registered=] through {{WindowSharedStorage/selectURL()}} with {{SharedStorageUrlWithMetadata/reportingMetadata}} {{/object}}s, if [=fence.reportEvent()=] is called on a [=FenceEvent=] with a [=FenceEvent/destination=] [=list/containing=] "`shared-storage-select-url`" and that [=FenceEvent=]'s corresponding [=FenceEvent/eventType=] is triggered, then the [=FenceEvent=]'s [=FenceEvent/eventData=] will be sent as a [=beacon=] to the registered [=/URL=] for that [=FenceEvent/eventType=].
<div algorithm>
To <dfn>validate reporting metadata</dfn>, given an {{/object}} |reportingMetadata|, run the following steps:
1. If |reportingMetadata| is [=map/empty=], return true.
1. If |reportingMetadata| is not a [=dictionary=], return false.
1. [=map/iterate|For each=] <var ignore="">eventType</var> -> |urlString| of |reportingMetadata|, if the result of running [=get the canonical URL string if valid=] with |urlString| is undefined, return false.
1. Return true.
</div>
<div algorithm>
To <dfn>get the canonical URL string if valid</dfn>, given a [=string=] |urlString|, run the following steps:
1. Let |url| be the result of running a [=URL parser=] on |urlString|.
1. If |url| is not a valid [=/URL=], return undefined.
1. Otherwise, return the result of running a [=URL serializer=] on |url|.
</div>
<div algorithm>
To <dfn>register reporting metadata</dfn>, given an {{/object}} |reportingMetadata| and a "fenced frame config struct" |fencedFrameConfigStruct|, run the following steps:
1. If |reportingMetadata| is [=map/empty=], return.
1. [=Assert=] that |reportingMetadata| is a [=dictionary=].
1. Let |reportingUrlMap| be an [=map/empty=] [=map=].
1. [=map/iterate|For each=] |eventType| -> |urlString| of |reportingMetadata|:
1. Let |url| be the result of running a [=URL parser=] on |urlString|.
1. [=Assert=] that |url| is a valid [=/URL=].
1. [=map/Set=] |reportingUrlMap|[|eventType|] to |url|.
1. <span class=todo>Store |reportingUrlMap| inside a "fenced frame reporter" class associated with |fencedFrameConfigStruct|. Both of these still need to be added to the draft [=fenced frame|Fenced Frame specification=].</span>
</div>
### Entropy Budgets ### {#budgets}
Because [=bits of entropy=] can leak via {{WindowSharedStorage/selectURL()}}, the [=user agent=] will need to maintain budgets to limit these leaks.
#### Navigation Entropy Budget #### {#nav-budget}
If a user activates a [=fenced frame=] whose {{FencedFrameConfig}} was generated by {{WindowSharedStorage/selectURL()}} and thereby initiates a [=top-frame=] [=navigate|navigation=], this will reveal to the landing page that its [=/URL=] was selected, which is a leak in [=entropy bits=] of up to logarithm base 2 of the number of input [=/URLs=] for the call to {{WindowSharedStorage/selectURL()}}. To mitigate this, a [=user agent=] will set a per-[=calling site=] [=navigation entropy allowance=].
A <dfn>calling site</dfn> for {{WindowSharedStorage/selectURL()}} is the [=site=] resulting from running [=obtain a site=] with the [=url/origin=] of an [=environment=] that makes a {{WindowSharedStorage/selectURL()}} call.
A <dfn>navigation entropy allowance</dfn> is a maximum allowance of [=entropy bits=] that are permitted to leak via [=fenced frames=] initiating [=top-frame=] [=navigate|navigations=] during a given [=navigation budget epoch=] for a given calling [=calling site=]. This [=navigation entropy allowance|allowance=] is defined by the [=user agent=] and is [=calling site=]-agnostic.
A [=user agent=] will define a fixed predetermined [=duration=] <dfn>navigation budget lifetime</dfn>.
An <dfn>navigation budget epoch</dfn> is any interval of time whose [=duration=] is the [=navigation budget lifetime=].
To keep track of how this [=navigation entropy allowance=] is used, the [=user agent=] uses a <dfn>shared storage navigation budget table</dfn>, which is a [=map=] of [=calling sites=] to [=navigation entropy ledgers=].
An <dfn>navigation entropy ledger</dfn> is a [=/list=] of [=bit debits=].
A <dfn>bit debit</dfn> is a [=struct=] containing a {{double}} <dfn for="bit debit">bits</dfn>, indicating a value in [=entropy bits=], along with a {{DOMHighResTimeStamp}} <dfn for="bit debit">timestamp</dfn> (from the [=Unix Epoch=]).
[=Bit debits=] whose [=bit debit/timestamps=] precede the start of the current [=navigation budget epoch=] are said to be <dfn for="bit debit">expired</dfn>.
When a leak occurs, its value in [=entropy bits=] is calculated and stored for that [=calling site=], along with the current time as a [=bit debit/timestamp=], together as a [=bit debit=] in the [=shared storage navigation budget table=].
A [=calling site=]'s <dfn for="calling site">remaining navigation budget</dfn> is the [=navigation entropy allowance=] minus any [=bit debits=] whose [=bit debit/timestamps=] are within the current [=navigation budget epoch=].
{{WindowSharedStorage/selectURL()}}'s argument "`urls`" is its <dfn for=selectURL>input URL list</dfn>.
When a [=calling site=] has insufficient [=calling site/remaining navigation budget=], {{WindowSharedStorage/selectURL()}} will return a {{SharedStorageResponse}} (i.e. either a {{FencedFrameConfig}} or an opaque [=/URL=]) for the {{SharedStorageUrlWithMetadata/url}} in the {{SharedStorageUrlWithMetadata}} at the [=default index=] in its [=selectURL/input URL list=].
The <dfn>default index</dfn> for a call to {{WindowSharedStorage/selectURL()}} is implementation-defined in such a way that it is independent from the result of the associated {{SharedStorageSelectURLOperation}}'s "`run`" method.
<div class="example">
The [=default index=] could be defined to be 0.
In this case, whenever the {{SharedStorageSelectURLOperation}}'s "`run`" method encounters an error, or whenever there is insufficient [=calling site/remaining navigation budget=], the "`run`" method would return 0, and hence {{WindowSharedStorage/selectURL()}} would return a {{SharedStorageResponse}} for the first {{SharedStorageUrlWithMetadata/url}} in its [=selectURL/input URL list=].
</div>
<div class="example">
The [=default index=] could be defined to be [=selectURL/input URL list=]'s [=list/size=] minus 1.
In this case, whenever the {{SharedStorageSelectURLOperation}}'s "`run`" method encounters an error, or whenever there is insufficient [=calling site/remaining navigation budget=], {{WindowSharedStorage/selectURL()}} would return a {{SharedStorageResponse}} for the last {{SharedStorageUrlWithMetadata/url}} in its [=selectURL/input URL list=].
</div>
<div algorithm>
To <dfn>determine remaining navigation budget</dfn>, given an [=environment settings object=] |environment| and a [=calling site=] |site|, run the following steps:
1. If |site| is an [=opaque origin=], return undefined.
1. Let |maxBits| be the [=user agent=]'s [=navigation entropy allowance=].
1. If the [=user agent=]'s [=shared storage navigation budget table=] does not [=map/contain=] |site|, then return |maxBits|.
1. Otherwise, let |ledger| be [=user agent=]'s [=shared storage navigation budget table=][|site|].
1. Let |debitSum| be 0.
1. [=map/iterate|For each=] [=list/item=] |bitDebit| in |ledger|, do the following steps:
1. Let |debit| be |bitDebit|'s [=bit debit/bits=].
1. If the result of running [=check whether a bit debit is expired=] with |environment| and |bitDebit| is false, then increment |debitSum| by |debit|.
1. Return |maxBits| minus |debitSum|.
</div>
<div algorithm>
To <dfn>check whether a bit debit is expired</dfn>, given an [=environment settings object=] |environment| and a [=bit debit=] |bitDebit|, run the following steps:
1. Let |epochLength| be the [=user agent=]'s [=navigation budget lifetime=].
1. Let |currentTime| be |environment|'s [=environment settings object/current wall time=].
1. Let |threshold| be |currentTime| minus |epochLength|.
1. If |bitDebit|'s [=bit debit/timestamp=] is less than |threshold|, return true.
1. Otherwise, return false.
</div>
A [=bit debit=] will need to be [=charge shared storage navigation budget|charged=] to the [=shared storage navigation budget table=] for each [=top-frame=] [=navigate|navigation=] initiated by a [=fenced frame=] whose {{FencedFrameConfig}} was generated via {{WindowSharedStorage/selectURL()}}, as this can leak cross-site data. Since the [=bit debit/bits=] to charge is calculated during the call to {{WindowSharedStorage/selectURL()}} but only actually recorded in the [=shared storage navigation budget table=] if and when the resulting fenced frame initiates a [=top-frame=] [=beginning navigation|navigation=], the [=bit debit/bits=] must be stored as a <dfn>pending shared storage budget debit</dfn> in the corresponding {{FencedFrameConfig}} until this time.
Between [=beginning navigation=] and [=ending navigation=], a [=user agent=] will perform the [=charge shared storage navigation budget=] algorithm.
Issue: The "fenced frame config struct" and its boolean <dfn>has navigated</dfn> have not yet been added to the draft [=Fenced Frame=] specification. Some form of them will be added, although their names are subject to bikeshedding. <span class=todo>Fix the names when they are added.</span>
<div algorithm>
To <dfn>charge shared storage navigation budget</dfn> during a [=beginning navigation|navigation=] with [=/navigable=] |navigable| and {{Document}} |sourceDocument|, run the following steps:
1. If |navigable| is not a [=navigable/traversable navigable=], return.
1. Let |node| be |sourceDocument|'s [=node navigable=].
1. While |node| is not null:
1. Let |site| be the result of running [=obtain a site=] with |node|'s [=active document=]'s [=document/origin=].
1. If |node| has a "fenced frame config struct" and |site| is not an [=opaque origin=], perform the following steps:
1. Let |pendingBits| be |node|'s "fenced frame config struct"'s [=pending shared storage budget debit=].
1. If |pendingBits| is greater than 0 and if "fenced frame config struct"'s [=has navigated=] is false, run the following steps:
1. Let |ledger| be [=user agent=]'s [=shared storage navigation budget table=][|site|].
1. Let |bitDebit| be a new [=bit debit=].
1. Set |bitDebit|'s [=bit debit/bits=] to |pendingBits|.
1. Let |currentTime| be the [=/current wall time=].
1. Set |bitDebit|'s [=bit debit/timestamp=] to |currentTime|.
1. [=list/Append=] |bitDebit| to |ledger|.
1. Set |node|'s "fenced frame config struct"'s [=has navigated=] to true.
1. Set |node| to |node|'s [=navigable/parent=].
</div>
#### Reporting Entropy Budget #### {#report-budget}
Likewise, each time a call to [=fence.reportEvent()=] from a [=fenced frame=] originating via {{WindowSharedStorage/selectURL()}} whose [=FenceEvent/destination=] [=list/contains=] "`shared-storage-select-url`" and whose [=FenceEvent/eventType=] is triggered, there is a leak of up to logarithm base 2 of the number of main input [=/URLs=] [=entropy bits=]. The [=user agent=] will need to set a per-[=page load=] [=reporting entropy allowance=] to restrict the information leaked, with <dfn>page load</dfn> referring to a [=top-level traversable=]'s (i.e. primary main frame's) lifecycle.
A <dfn>reporting entropy allowance</dfn> is a maximum allowance of [=entropy bits=] that are permitted to leak via [=fence.reportEvent()=] during a given page load. This [=reporting entropy allowance|allowance=] is defined by the [=user agent=].
Each [=top-level traversable=] will have a new {{double}} <dfn>shared storage reporting budget</dfn> associated to it which will be initialized with the value of [=user agent=]'s [=reporting entropy allowance=] upon [=top-level traversable=]'s creation.
When [=fence.reportEvent()=] is called with a [=FenceEvent/destination=] [=list/containing=] "`shared-storage-select-url`", it will be necessary to [=charge shared storage reporting budget=] as below.
<div algorithm>
To <dfn>determine reporting budget to charge</dfn>, given a {{Document}} |sourceDocument|, run the following steps:
1. Let |debitSum| be 0.
1. Let |node| be |sourceDocument|'s [=node navigable=].
1. While |node| is not null:
1. If |node| has a "fenced frame config struct":
1. Let |pendingBits| be |node|'s "fenced frame config struct"'s [=pending shared storage budget debit=].
1. If |pendingBits| is greater than 0 and if "fenced frame config struct"'s [=has reported=] is false, increment |debitSum| by |pendingBits|
1. Set |node| to |node|'s [=navigable/parent=].
1. Return |debitSum|.
</div>
Issue: The "fenced frame config struct" and its boolean <dfn>has reported</dfn> have not yet been added to the draft [=Fenced Frame=] specification. Some form of them will be added, although their names are subject to bikeshedding. <span class=todo>Fix the names when they are added.</span>
<div algorithm>
To <dfn>charge shared storage reporting budget</dfn> given a {{Document}} |sourceDocument|, run the following steps:
1. Let |toCharge| be the result of running [=determine reporting budget to charge=] with |sourceDocument|.
1. Let |node| be |sourceDocument|'s [=node navigable=].
1. Let |topNode| be the result of running [=get the top-level traversable=] for |node|.
1. If |topNode|'s [=shared storage reporting budget=] is less than |toCharge|, return false.
1. While |node| is not null:
1. If |node| has a "fenced frame config struct" and if |node|'s "fenced frame config struct"'s [=pending shared storage budget debit=] is greater than 0, set |node|'s "fenced frame config struct"'s [=has reported=] to true.
1. Set |node| to |node|'s [=navigable/parent=].
1. Decrement |topNode|'s [=shared storage reporting budget=] by |toCharge|.
1. Return true.
</div>
A [=user agent=] may wish to set a timer to periodically [=purge expired bit debits from all navigation entropy ledgers=], as the [=bit debit/expired=] [=bit debits=] will no longer be needed.
<div algorithm>
To <dfn>purge expired bit debits from all navigation entropy ledgers</dfn>, run the following steps:
1. [=map/iterate|For each=] <var ignore="">origin</var> -> |ledger| of [=user agent=]'s [=shared storage navigation budget table=]:
1. [=map/iterate|For each=] |bitDebit| in |ledger|, if the result of running [=check whether a bit debit is expired=] with |bitDebit| is true, [=list/remove=] |bitDebit| from |ledger|.
</div>
### Run Operation Methods ### {#run-op}
<div algorithm>
The <dfn method for="WindowSharedStorage">run(|name|, |options|)</dfn> method steps are:
1. Let |promise| be a new [=promise=].
1. Let |worklet| be {{WindowSharedStorage}}'s {{WindowSharedStorage/worklet}}.
1. If |worklet|'s [=global scopes|list of global scopes=] is [=list/empty=], then return a [=promise rejected=] with a {{TypeError}}.
1. [=Assert=] that |worklet|'s [=global scopes|list of global scopes=] [=list/contains=] a single {{SharedStorageWorkletGlobalScope}}.
1. If the result of running [=SharedStorageWorkletGlobalScope/check whether addModule is finished=] for |worklet|'s {{SharedStorageWorkletGlobalScope}} is false, return a [=promise rejected=] with a {{TypeError}}.
1. Let |realm| be the [=current realm=].
1. Let |outsideSettings| be {{WindowSharedStorage/worklet}}'s [=relevant settings object=].
1. If the result of running [=determine whether shared storage is allowed=] on |outsideSettings| is false, return a [=promise rejected=] with a {{TypeError}}.
1. Let |agent| be the result of [=obtaining a worklet agent=] given |outsideSettings|.
1. Run the following steps in |agent|:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with undefined.
1. If {{WindowSharedStorage/worklet}}'s [=module map=] is not [=map/empty=]:
1. Let |operationMap| be this {{SharedStorageWorkletGlobalScope}}'s [=SharedStorageWorkletGlobalScope/operation map=].
1. If |operationMap| [=map/contains=] |name|:
1. Let |operation| be |operationMap|[|name|].
1. If |options| [=map/contains=] |data|:
1. Let |argumentsList| be a new [=/list=].
1. [=list/Append=] |data| to |argumentsList|.
1. [=Call=] |operation| with |argumentsList|.
1. Otherwise, [=call=] |operation| without any arguments list.
1. If |options|["`keepAlive`"] is false:
1. Wait for |operation| to finish running, if applicable.
1. Run [=terminate a worklet global scope=] with {{SharedStorageWorkletGlobalScope}}.
1. Return |promise|.
</div>
<div algorithm>
To <dfn>get the select-url result index</dfn>, given {{WindowSharedStorage/worklet}} |worklet|, {{DOMString}} |operationName|, [=/list=] |urlList|, and {{SharedStorageRunOperationMethodOptions}} |options|:
1. Let |promise| be a new [=promise=].
1. Let |context| be {{WindowSharedStorage}}'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. If |environment|'s [=associated document=] is not [=fully active=], return a [=promise rejected=] with a {{TypeError}}.
1. Let |realm| be the [=current realm=].
1. Let |outsideSettings| be |worklet|'s [=relevant settings object=].
1. Let |agent| be the result of [=obtaining a worklet agent=] given |outsideSettings|.
1. Run the following steps in |agent|:
1. Let |index| be [=default index=].
1. If {{WindowSharedStorage/worklet}}'s [=module map=] is not [=map/empty=]:
1. Let |operationMap| be the associated {{SharedStorageWorkletGlobalScope}}'s [=SharedStorageWorkletGlobalScope/operation map=].
1. If |operationMap| [=map/contains=] |operationName|:
1. Let |operation| be |operationMap|[|operationName|].
1. Let |argumentsList| be a new [=/list=] with a single entry [=list/contain|containing=] |urlList|.
1. If |options| [=map/contains=] |data|, [=list/append=] |data| to |argumentsList|.
1. Let |operationResult| be the result of running [=Call=] on |operation| with |argumentsList|.
1. If |operationResult| has any error(s), then [=queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to reject |promise| with a {{TypeError}}.
1. Otherwise:
1. Set |index| to the result of [=casting=] |operationResult| to an {{unsigned long}}.
1. If this throws an exception:
1. Catch it and [=queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to reject |promise| with a {{TypeError}}.
1. Abort these steps.
1. Otherwise, if |index| is greater than |urlList|'s [=list/size=]:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to reject |promise| with a {{TypeError}}.
1. Abort these steps.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to resolve |promise| with |index|.
1. Return |promise|.
</div>
<div algorithm>
The <dfn method for="WindowSharedStorage">selectURL(|name|, |urls|, |options|)</dfn> method steps are:
1. Let |resultPromise| be a new [=promise=].
1. Let |context| be {{WindowSharedStorage}}'s {{Window}}'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |context|'s [=active window=]'s [=relevant settings object=].
1. If the result of running [=determine whether shared storage is allowed=] on |environment| is false, return a [=promise rejected=] with a {{TypeError}}.
1. Let |document| be |context|'s [=active document=].
1. If |document| is not [=allowed to use=] the "[=PermissionsPolicy/shared-storage-select-url=]" feature, return a [=promise rejected=] with a {{TypeError}}.
1. Let |worklet| be {{WindowSharedStorage}}'s {{WindowSharedStorage/worklet}}.
1. If |worklet|'s [=global scopes|list of global scopes=] is [=list/empty=], then return a [=promise rejected=] with a {{TypeError}}.
1. [=Assert=] that |worklet|'s [=global scopes|list of global scopes=] [=list/contains=] a single {{SharedStorageWorkletGlobalScope}}.
1. If the result of running [=SharedStorageWorkletGlobalScope/check whether addModule is finished=] for |worklet|'s {{SharedStorageWorkletGlobalScope}} is false, return a [=promise rejected=] with a {{TypeError}}.
1. If |urls| is empty or exceeds the maximum allowed length, return a [=promise rejected=] with a {{TypeError}}.
1. Let |urlList| be an empty {{list}}.
1. [=map/iterate|For each=] |urlWithMetadata| in |urls|:
1. If |urlWithMetadata| has no field "`url`", return a [=promise rejected=] with a {{TypeError}}.
1. Otherwise, let |urlString| be |urlWithMetadata|["`url`"].
1. Let |serializedUrl| be the result of running [=get the canonical URL string if valid=] with |urlString|.
1. If |serializedUrl| is undefined, return a [=promise rejected=] with a {{TypeError}}.
1. Otherwise, [=list/append=] |serializedUrl| to |urlList|.
1. If |urlWithMetadata| has field "`reportingMetadata`":
1. Let |reportingMetadata| be |urlWithMetadata|["`reportingMetadata`"].
1. If the result of running [=validate reporting metadata=] with |reportingMetadata| is false, reject |resultPromise| with a {{TypeError}} and abort these steps.
1. Let |fencedFrameConfigStruct| be a "fenced frame config struct". <span class=todo>Add correct struct name as well as linking when Fenced Frame API updates their draft spec to include it.</span>
Issue: The "fenced frame config struct" and the following "obtain a {{FencedFrameConfig}} from a fenced frame config struct" algorithm have not yet been added to the draft [=Fenced Frame=] specification. Some form of them will be added, although their names are subject to bikeshedding.
1. If |options|["`resolveToConfig`"] is true, resolve |resultPromise| with the result of running "obtain a {{FencedFrameConfig}} from a fenced frame config struct" with |fencedFrameConfigStruct|. <span class=todo>Add correct struct and algorithms names as well as linking when Fenced Frame API updates their draft spec to include it.</span>
1. Othewise, resolve |resultPromise| to |fencedFrameConfigStruct|'s "urn uuid". <span class=todo>Add correct struct name and urn:uuid name as well as linking when Fenced Frame API updates their draft spec to include it.</span>
1. Let |indexPromise| be the result of running [=get the select-url result index=], given |worklet|, |name|, |urlList|, and |options|.
1. [=Upon fulfillment=] of |indexPromise|, perform the following steps:
1. Let |resultIndex| be the numerical value of |indexPromise|.
1. Let |site| be the result of running [=obtain a site=] with |document|'s [=url/origin=].
1. Let |remainingBudget| be the result of running [=determine remaining navigation budget=] with |environment| and |site|.
1. [=Assert=] that |remainingBudget| is not undefined.
1. Let |pendingBits| be the logarithm base 2 of |urlList|'s [=list/size=].
1. If |pendingBits| is greather than |remainingBudget|, set |resultIndex| to [=default index=].
1. Set |fencedFrameConfigStruct|'s [=pending shared storage budget debit=] to |pendingBits|.
1. Set |fencedFrameConfigStruct|'s [=/url=] to |urlList|[|resultIndex|].
1. Let |resultURLWithMetadata| be |urls|[|resultIndex|].
1. If |resultURLWithMetadata| has field "`reportingMetadata`", run [=register reporting metadata=] with |resultURLWithMetadata|["`reportingMetadata`"].
1. If |options|["`keepAlive`"] is false, run [=terminate a worklet global scope=] with the associated {{SharedStorageWorkletGlobalScope}}.
1. [=Upon rejection=] of |indexPromise|, perform the following steps:
1. Set |fencedFrameConfigStruct|'s [=/url=] to |urlList|[[=default index=]].
1. If |options|["`keepAlive`"] is false, run [=terminate a worklet global scope=] with the associated {{SharedStorageWorkletGlobalScope}}.
1. Return |resultPromise|.
</div>
## Extension to the {{Window}} interface ## {#extension}
Each {{Window}} object has an associated {{WindowSharedStorage}} instance {{Window/sharedStorage}}, which is created alongside the .{{Window}} if [=Shared Storage=] is enabled, with the [=Window/sharedStorage getter|getter=] below.
<xmp class='idl'>
partial interface Window {
[SecureContext] readonly attribute WindowSharedStorage? sharedStorage;
};
</xmp>
<div algorithm>
The <dfn for="Window">{{Window/sharedStorage}} getter</dfn> steps are:
1. If |this| is [=fully active=], return |this|'s {{Window/sharedStorage}}.
1. Otherwise, return null.
</div>
## The {{WorkletSharedStorage}} interface ## {#worklet-shared-storage}