-
Notifications
You must be signed in to change notification settings - Fork 24
/
spec.bs
2045 lines (1632 loc) · 143 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
Repository: WICG/shared-storage
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-level traversable 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
type:dfn;
for:/; text:list
spec:webidl;
type:interface;
text:double
type:dfn;
text:an exception was thrown
spec:html;
type:dfn;
for:realm; text:global object
for:WorkerGlobalScope; text:module map
for:navigable; text:top-level traversable
spec:fenced-frame;
type:dfn;
for:fencedframetype; text:fenced frame reporter
for:browsing context; text:fenced frame config instance
</pre>
<pre class="anchors">
urlPrefix: https://www.ietf.org/rfc/rfc4122.txt
type: dfn; text: urn uuid
spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
type: dfn
text: worklets; url: worklets.html#worklets
text: added modules list; url: worklets.html#concept-worklet-added-modules-list
text: set up a worklet environment settings object; url: worklets.html#set-up-a-worklet-environment-settings-object
text: fetch a worklet/module worker script graph; url: webappapis.html#fetch-a-worklet/module-worker-script-graph
text: fetch a worklet script graph; url: webappapis.html#fetch-a-worklet-script-graph
text: processCustomFetchResponse; url: webappapis.html#fetching-scripts-processcustomfetchresponse
text: environment; url: webappapis.html#environment
text: worklet event loop; url: webappapis.html#worklet-event-loop
text: obtaining a worklet agent; url: webappapis.html#obtain-a-worklet-agent
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
text: serialization; for: origin; url: browsers.html#ascii-serialisation-of-an-origin
text: initialize the navigable; url: document-sequences.html#initialize-the-navigable
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
text: promise resolved; url: a-promise-resolved-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: Get; url: sec-get-o-p
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://patcg-individual-drafts.github.io/private-aggregation-api/
type: dfn
text: Private Aggregation; url:
text: get the privateAggregation
text: determine if an origin is an aggregation coordinator
text: pre-specified report parameters
for: pre-specified report parameters
text: context ID
text: filtering ID max bytes
text: batching scope
text: debug scope
text: process contributions for a batching scope
text: set the aggregation coordinator for a batching scope
text: determine if a report should be sent deterministically
text: mark a debug scope complete
text: set the pre-specified report parameters for a batching scope
text: aggregation coordinator
text: default filtering id max bytes
text: valid filtering id max bytes range
text: context id
text: scoping details
for: scoping details
text: get batching scope steps
text: get debug scope steps
text: private-aggregation
for: PrivateAggregation
text: allowed to use
text: scoping details; url: #privateaggregation-scoping-details
type: interface
text: PrivateAggregation
spec: protected-audience; urlPrefix: https://wicg.github.io/turtledove/
type: dfn
text: get storage interest groups for owner
type: interface
text: StorageInterestGroup; url: dictdef-storageinterestgroup
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
spec: UUID; urlPrefix: https://www.ietf.org/rfc/rfc4122.txt
type: dfn
text: urn uuid; url: urn-uuid
spec: hr-time; urlPrefix: https://w3c.github.io/hr-time/
type: dfn
text: current wall time; url: dfn-current-wall-time
</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-level traversable=] 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-level traversable=] 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=] 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 {{SharedStorageWorklet/selectURL()|URL-selection}} output gate.
<xmp class='idl'>
typedef (USVString or FencedFrameConfig) SharedStorageResponse;
</xmp>
<xmp class='idl'>
[Exposed=(Window)]
interface SharedStorageWorklet : Worklet {
Promise<SharedStorageResponse> selectURL(DOMString name,
sequence<SharedStorageUrlWithMetadata> urls,
optional SharedStorageRunOperationMethodOptions options = {});
Promise<any> run(DOMString name,
optional SharedStorageRunOperationMethodOptions options = {});
};
</xmp>
Each {{SharedStorageWorklet}} has an associated boolean <dfn for="SharedStorageWorklet">addModule initiated</dfn>, initialized to false.
Each {{SharedStorageWorklet}} has an associated {{USVString}} <dfn for="SharedStorageWorklet">data origin</dfn>, initialized to `"context-origin"`.
Each {{SharedStorageWorklet}} has an associated boolean <dfn for="SharedStorageWorklet">has cross-origin data origin</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 state=], and if the result is "DisallowedDueToNonPreferenceError", or if the result is "DisallowedDueToPreferenceError" and the worklet's [=SharedStorageWorklet/has cross-origin data origin=] is false, it will cause {{Worklet/addModule()}} to fail, as detailed in the [[#add-module-monkey-patch]].
<div algorithm>
To <dfn>check if user preference setting allows access to shared storage</dfn> given an [=environment settings object=] |environment| and an [=/origin=] |origin|, run the following step:
1. Using values available in |environment| and |origin| as needed, perform an [=implementation-defined=] algorithm to return either true or false.
</div>
<div algorithm>
To <dfn>determine whether shared storage is allowed by context</dfn>, given an [=environment settings object=] |environment|, an [=/origin=] |origin|, and a boolean |allowedInOpaqueOriginContext|, run these steps:
1. If |environment| is not a [=secure context=], then return false.
1. If |allowedInOpaqueOriginContext| is false and |environment|'s [=environment settings object/origin=] is an [=opaque origin=], then return false.
1. If |origin| is an [=opaque origin=], then return false.
1. Let |globalObject| be the [=current realm=]'s [=global object=].
1. [=Assert=]: |globalObject| is a {{Window}} or a {{SharedStorageWorkletGlobalScope}}.
1. If |globalObject| is a {{Window}}, and if the result of running [=Is feature enabled in document for origin?=] on "[=PermissionsPolicy/shared-storage=]", |globalObject|'s [=associated document=], and |origin| returns false, then return false.
1. If the result of running [=obtaining a site|obtain a site=] with |origin| is not [=enrolled=], then return false.
1. Return true.
</div>
<div class="note">
Here are the scenarios where the algorithms [=determine whether shared storage is allowed by context=] and [=check if user preference setting allows access to shared storage=] are used:
- For creating a worklet, |environment| is the [=environment settings object=] associated with the {{Window}} that created the worklet, and |origin| is the module script url's [=url/origin=].
- For running operations on a worklet (from a {{Window}}), |environment| is the [=environment settings object=] associated with the {{Window}} that created the worklet, and |origin| is the worklet's [=global scopes=][0]'s [=global object/realm=]'s [=realm/settings object=]'s [=environment settings object/origin=].
- For [[#setter]], |environment| is either the current context (when called from a {{Window}}) or the [=environment settings object=] associated with the {{Window}} that created the worklet (when called from a {{SharedStorageWorkletGlobalScope}}), and |origin| is |environment|'s [=environment settings object/origin=].
- For [[#ss-fetch-algo]], |environment| is the request's [=request/window=], and |origin| is the request's [=request/current URL=]'s [=url/origin=].
- For [[#ss-fetch-algo]], for {{SharedStorage/createWorklet()}} called with a cross-origin worklet script using the <var ignore=''>dataOrigin</var> option with value `"script-origin"` (which would result in a worklet where [=SharedStorageWorklet/has cross-origin data origin=] is true), and for {{SharedStorageWorklet/selectURL()}} and {{SharedStorageWorklet/run()}} that operate on a worklet where [=SharedStorageWorklet/has cross-origin data origin=] is true, |allowedInOpaqueOriginContext| is true. For other methods, |allowedInOpaqueOriginContext| is false.
</div>
<div algorithm>
To <dfn>check if addModule is allowed and update state</dfn> given a {{SharedStorageWorklet}} |worklet| and a [=/URL=] |moduleURLRecord|, run the following steps:
1. If |worklet|'s [=addModule initiated=] is true, return "DisallowedDueToNonPreferenceError".
1. Set |worklet|'s [=addModule initiated=] to true.
1. Let |workletDataOrigin| be the [=current settings object=]'s [=environment settings object/origin=].
1. If |worklet|'s [=SharedStorageWorklet/data origin=] is `"script-origin"`, set |workletDataOrigin| to |moduleURLRecord|'s [=url/origin=].
1. Otherwise, if |worklet|'s [=SharedStorageWorklet/data origin=] is not `"context-origin"`:
1. Let |customOriginUrl| be the result of running a [=URL parser=] on |worklet|'s [=SharedStorageWorklet/data origin=].
1. If |customOriginUrl| is not a valid [=/URL=], return "DisallowedDueToNonPreferenceError".
1. Set |workletDataOrigin| to |customOriginUrl|'s [=url/origin=].
1. Let |hasCrossOriginDataOrigin| be false.
1. If |workletDataOrigin| and the [=current settings object=]'s [=environment settings object/origin=] are not [=same origin=], then set |hasCrossOriginDataOrigin| to true.
1. Let |allowedInOpaqueOriginContext| be |hasCrossOriginDataOrigin|.
1. If the result of running [=determine whether shared storage is allowed by context=] given the [=current settings object=], |workletDataOrigin|, and |allowedInOpaqueOriginContext| is false, return "DisallowedDueToNonPreferenceError".
1. Set |worklet|'s [=SharedStorageWorklet/has cross-origin data origin=] to |hasCrossOriginDataOrigin|.
1. If the result of running [=check if user preference setting allows access to shared storage=] given the [=current settings object=] and |workletDataOrigin| is false, return "DisallowedDueToPreferenceError".
1. Return "Allowed".
</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}}.
## Run Operation Methods on {{SharedStorageWorklet}} ## {#run-op-shared-storage-worklet}
<div algorithm>
To <dfn>get the select-url result index</dfn>, given {{SharedStorageWorklet}} |worklet|, {{DOMString}} |operationName|, [=/list=] of [=strings=] |urlList|, an [=/origin=] |workletDataOrigin|, a [=/navigable=] |navigable|, {{SharedStorageRunOperationMethodOptions}} |options|, a [=pre-specified report parameters=] or null |preSpecifiedParams| and an [=aggregation coordinator=] or null |aggregationCoordinator|, run the following steps. This algorithm will return a [=tuple=] consisting of a [=promise=] that resolves into an {{unsigned long}} whose value is the index of the URL selected from |urlList| and a [=/boolean=] indicating whether the [top-level traversable's budgets](#charge-top-trav-budgets) should be [=charge shared storage top-level traversable budgets|charged=].
1. Let |promise| be a new [=promise=].
1. Let |window| be |worklet|'s [=relevant settings object=].
1. [=Assert=]: |window| is a {{Window}}.
1. If |window|'s [=Window/browsing context=] is null, then return the [=tuple=] of a ([=promise rejected=] with a {{TypeError}}, true).
1. If |window|'s [=associated document=] is not [=fully active=], return the [=tuple=] of a ([=promise rejected=] with a {{TypeError}}, true).
1. [=Assert=]: |worklet|'s [=global scopes=]'s [=list/size=] is 1.
1. Let |globalScope| be |worklet|'s [=global scopes=][0].
1. Let |moduleMapKeyTuples| be the result of running [=map/get the keys=] on |globalScope|'s [=relevant settings object=]'s [=module map=].
1. [=Assert=]: |moduleMapKeyTuples| has [=map/size=] 1.
1. Let |moduleURLRecord| be |moduleMapKeyTuples|[0][0].
1. Let |savedQueryName| be |options|["`savedQuery`"].
1. If |savedQueryName| is a [=string=] that is not the empty string, then:
1. Let |callbackTask| be the result of running [=obtain a callback to process the saved index result=], given |window|, |urlList|, and |promise|.
1. Let |savedIndex| be the result of running [=get the index for a saved query=] on |navigable|, |workletDataOrigin|, |moduleURLRecord|, |operationName|, |savedQueryName|, and |callbackTask|.
1. If |savedIndex| is "pending callback", then return the [=tuple=] (|promise|, false).
Note: |callbackTask| is now stored to be run when a previously obtained worklet agent completes its operation to select the index for this query. When the steps of |callbackTask| are run, |promise| will be resolved.
1. If |savedIndex| is an {{unsigned long}}, then:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |window|, to run the steps of |callbackTask|, given |savedIndex|.
Note: Running the steps of |callbackTask| will resolve |promise|.
1. Return the [=tuple=] (|promise|, false).
1. [=Assert=] that |savedIndex| is "pending current operation".
1. [=Queue a task=] on |globalScope|'s [=worklet event loop=] to perform the following steps:
1. Let |operationMap| be |globalScope|'s [=SharedStorageWorkletGlobalScope/operation map=].
1. If |operationMap| does not [=map/contain=] |operationName|, then [=queue a global task=] on the [=DOM manipulation task source=], given |window|, to [=reject=] |promise| with a {{TypeError}}, and abort these steps.
Note: This could happen if {{SharedStorageWorkletGlobalScope/register()}} was never called with |operationName|.
1. [=Assert=]: |operationMap|[|operationName|]'s [=associated realm=] is [=this=]'s [=relevant realm=].
1. Let |operation| be |operationMap|[|operationName|], [=converted to an IDL value|converted=] to {{RunFunctionForSharedStorageSelectURLOperation}}.
1. Let |privateAggregationCompletionTask| be the result of [=setting up the Private Aggregation scopes=] given |workletDataOrigin|, |preSpecifiedParams| and |aggregationCoordinator|.
1. Let |argumentsList| be the [=/list=] « |urlList| ».
1. If |options|["{{SharedStorageRunOperationMethodOptions/data}}"] [=map/exists=], [=list/append=] it to |argumentsList|.
1. Let |indexPromise| be the result of [=invoking=] |operation| with |argumentsList|.
1. [=promise/React=] to |indexPromise|:
<dl class="switch">
: If it was fulfilled with value |index|:
:: 1. If |index| is greater than |urlList|'s [=list/size=], then:
1. If |savedQueryName| is a [=string=] that is not the empty string, then run [=store the index for a saved query=] with |window|, |navigable|, |workletDataOrigin|, |moduleURLRecord|, |operationName|, |savedQueryName|, and the [=default selectURL index=].
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |window|, to [=reject=] |promise| with a {{TypeError}}, and abort these steps.
Note: The result index is beyond the input urls' size. This violates the selectURL() protocol, and we don't know which url should be selected.
Otherwise:
1. If |savedQueryName| is a [=string=] that is not the empty string, then run [=store the index for a saved query=] with |window|, |navigable|, |workletDataOrigin|, |moduleURLRecord|, |operationName|, |savedQueryName|, and |index|.
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |window|, to [=resolve=] |promise| with |index|.
1. Run |privateAggregationCompletionTask|.
: If it was rejected:
:: 1. If |savedQueryName| is a [=string=] that is not the empty string, then run [=store the index for a saved query=] with |window|, |navigable|, |workletDataOrigin|, |moduleURLRecord|, |operationName|, |savedQueryName|, and the [=default selectURL index=].
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |window|, to [=reject=] |promise| with a {{TypeError}}.
Note: This indicates that either |operationCtor|'s run() method encounters an error (where |operationCtor| is the parameter in {{SharedStorageWorkletGlobalScope/register()}}), or the result |index| is a non-integer value, which violates the selectURL() protocol, and we don't know which url should be selected.
1. Run |privateAggregationCompletionTask|.
</dl>
1. Return the [=tuple=] (|promise|, true).
</div>
<div algorithm>
To <dfn>handle the result of selecting an index</dfn>, given a {{SharedStorageWorklet}} |worklet|, an [=environment settings object=] |environment|, a {{Document}} |document|, a {{sequence}} of {{SharedStorageUrlWithMetadata}} |urls|, a [=list=] of [=strings=] |urlList|, a [=/navigable=] |navigable|, a {{SharedStorageRunOperationMethodOptions}} |options|, a [=traversable navigable/fenced frame config mapping=] |fencedFrameConfigMapping|, a [=urn uuid=] |urn|, a [=/boolean=] |shouldChargeTopLevelBudgets|, a [=/boolean=] |shouldUseDefaultIndex|, and an {{unsigned long}} |resultIndex|, perform the following steps:
1. Let |site| be the result of running [=obtain a site=] with |document|'s [=Document/origin=].
1. Let |remainingBudget| be the result of running [=determine remaining navigation budget=] with |environment| and |site|.
1. Let |pendingBits| be the logarithm base 2 of |urlList|'s [=list/size=].
1. If |shouldChargeTopLevelBudgets| is true:
1. Let |pageBudgetResult| be the result of running [=charge shared storage top-level traversable budgets=] with |navigable|, |site|, and |pendingBits|.
1. If |pageBudgetResult| is false, set |shouldUseDefaultIndex| to true.
1. If |pendingBits| is greather than |remainingBudget|, set |shouldUseDefaultIndex| to true.
1. If |shouldUseDefaultIndex| is true, set |resultIndex| to the [=default selectURL index=].
1. Let |finalConfig| be a new [=fenced frame config=].
1. Set |finalConfig|'s [=fenced frame config/mapped url=] to |urlList|[|resultIndex|].
1. Set |finalConfig|'s <span class=todo>a "pending shared storage budget debit" field</span> to |pendingBits|.
1. [=Finalize a pending config=] on |fencedFrameConfigMapping| with |urn| and |finalConfig|.
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 |worklet|.
</div>
<div algorithm>
The <dfn method for="SharedStorageWorklet">selectURL(|name|, |urls|, |options|)</dfn> method steps are:
1. Let |resultPromise| be a new [=promise=].
1. If [=this=]'s [=addModule initiated=] is false, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |window| be [=this=]'s [=relevant settings object=].
1. [=Assert=]: |window| is a {{Window}}.
1. Let |context| be |window|'s [=Window/browsing context=].
1. If |context| is null, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |preSpecifiedParams| be the result of [=obtaining the pre-specified
report parameters=] given |options| and |context|.
1. If |preSpecifiedParams| is a {{DOMException}}, return [=a promise rejected
with=] |preSpecifiedParams|.
1. Let |aggregationCoordinator| be the result of [=obtaining the aggregation
coordinator=] given |options|.
1. If |aggregationCoordinator| is a {{DOMException}}, return [=a promise
rejected with=] |aggregationCoordinator|.
1. Let |document| be |context|'s [=active document=].
1. If [=this=]'s [=global scopes=] is [=list/empty=], then return a [=promise rejected=] with a {{TypeError}}.
Note: This can happen if {{SharedStorageWorklet/selectURL()}} is called before {{addModule()}}.
1. [=Assert=]: [=this=]'s [=global scopes=]'s [=list/size=] is 1.
1. Let |globalScope| be [=this=]'s [=global scopes=][0].
1. Let |workletDataOrigin| be |globalScope|'s [=global object/realm=]'s [=realm/settings object=]'s [=environment settings object/origin=].
1. If the result of running [=Is feature enabled in document for origin?=] on "[=PermissionsPolicy/shared-storage-select-url=]", |document|, and |workletDataOrigin| returns false, return a [=promise rejected=] with a {{TypeError}}.
1. If the result of running [=SharedStorageWorkletGlobalScope/check whether addModule is finished=] for |globalScope| is false, return a [=promise rejected=] with a {{TypeError}}.
1. If |urls| is empty or if |urls|'s [=list/size=] is greater than 8, return a [=promise rejected=] with a {{TypeError}}.
Note: 8 is chosen here so that each call of {{SharedStorageWorklet/selectURL()}} can leak at most log2(8) = 3 bits of information when the result fenced frame is clicked. It's not a lot of information per-call.
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 |navigable| be |window|'s [=associated document=]'s [=node navigable=].
1. Let |fencedFrameConfigMapping| be |navigable|'s [=navigable/traversable navigable=]'s [=traversable navigable/fenced frame config mapping=].
1. Let |pendingConfig| be a new [=fenced frame config=].
1. Let |urn| be the result of running [=fenced frame config mapping/store a pending config=] on |fencedFrameConfigMapping| with |pendingConfig|.
1. If |urn| is failure, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |environment| be |window|'s [=relevant settings object=].
1. Let |allowedInOpaqueOriginContext| be [=this=]'s [=SharedStorageWorklet/has cross-origin data origin=].
1. If the result of running [=determine whether shared storage is allowed by context=] given |environment|, |workletDataOrigin|, and |allowedInOpaqueOriginContext| is false, return a [=promise rejected=] with a {{TypeError}}.
1. If the result of running [=check if user preference setting allows access to shared storage=] given |environment| and |workletDataOrigin| is false:
1. If [=this=]'s [=SharedStorageWorklet/has cross-origin data origin=] is false, return a [=promise rejected=] with a {{TypeError}}.
1. If |options|["`resolveToConfig`"] is true, [=resolve=] |resultPromise| with |pendingConfig|.
1. Otherwise, [=resolve=] |resultPromise| with |urn|.
1. Let (|indexPromise|, |shouldChargeTopLevelBudgets|) be the result of running [=get the select-url result index=], given [=this=], |name|, |urlList|, |workletDataOrigin|, |navigable|, |options|, |preSpecifiedParams| and |aggregationCoordinator|.
1. [=Upon fulfillment=] of |indexPromise| with |resultIndex|, run [=handle the result of selecting an index=] given |worklet|, |environment|, |document|, |urls|, |urlList|, |navigable|, |options|, |fencedFrameConfigMapping|, |urn|, |shouldChargeTopLevelBudgets|, false, and |resultIndex|.
1. [=Upon rejection=] of |indexPromise|, run [=handle the result of selecting an index=] given |worklet|, |environment|, |document|, |urls|, |urlList|, |navigable|, |options|, |fencedFrameConfigMapping|, |urn|, |shouldChargeTopLevelBudgets|, true, and the [=default selectURL index=].
1. Return |resultPromise|.
</div>
<div algorithm>
The <dfn method for="SharedStorageWorklet">run(|name|, |options|)</dfn> method steps are:
1. Let |promise| be a new [=promise=].
1. If [=this=]'s [=addModule initiated=] is false, then return a [=promise rejected=] with a {{TypeError}}.
1. Let |window| be [=this=]'s [=relevant settings object=].
1. [=Assert=]: |window| is a {{Window}}.
1. Let |context| be |window|'s [=Window/browsing context=].
1. If |context| is null, then return [=a promise rejected with=] a
{{TypeError}}.
1. Let |preSpecifiedParams| be the result of [=obtaining the pre-specified
report parameters=] given |options| and |context|.
1. If |preSpecifiedParams| is a {{DOMException}}, return [=a promise rejected
with=] |preSpecifiedParams|.
1. Let |aggregationCoordinator| be the result of [=obtaining the aggregation
coordinator=] given |options|.
1. If |aggregationCoordinator| is a {{DOMException}}, return [=a promise
rejected with=] |aggregationCoordinator|.
1. If [=this=]'s [=global scopes=] is [=list/empty=], then return a [=promise rejected=] with a {{TypeError}}.
Note: This can happen if {{SharedStorageWorklet/run()}} is called before {{addModule()}}.
1. [=Assert=]: [=this=]'s [=global scopes=]'s [=list/size=] is 1.
1. Let |globalScope| be [=this=]'s [=global scopes=][0].
1. If the result of running [=SharedStorageWorkletGlobalScope/check whether addModule is finished=] for |globalScope| is false, return a [=promise rejected=] with a {{TypeError}}.
1. Let |workletDataOrigin| be |globalScope|'s [=global object/realm=]'s [=realm/settings object=]'s [=environment settings object/origin=].
1. Let |allowedInOpaqueOriginContext| be [=this=]'s [=SharedStorageWorklet/has cross-origin data origin=].
1. If the result of running [=determine whether shared storage is allowed by context=] given |window|, |workletDataOrigin|, and |allowedInOpaqueOriginContext| is false, [=reject=] |promise| with a {{TypeError}}.
1. If the result of running [=check if user preference setting allows access to shared storage=] given |window| and |workletDataOrigin| is false:
1. If [=this=]'s [=SharedStorageWorklet/has cross-origin data origin=] is false, [=reject=] |promise| with a {{TypeError}}.
1. Else, [=resolve=] |promise| with undefined.
1. Return |promise|.
1. Return |promise|, and immediately [=obtaining a worklet agent=] given |window| and run the rest of these steps in that agent:
Note: The |promise|'s resolution should be before and not depend on the execution inside {{SharedStorageWorkletGlobalScope}}. This is because shared storage is a type of unpartitioned storage, and a {{SharedStorageWorkletGlobalScope}} can have access to cross-site data, which shouldn't be leaked via {{SharedStorageWorklet/run()}} (via its success/error result).
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |window|, to [=resolve=] |promise| with undefined.
1. If |globalScope|'s [=relevant settings object=]'s [=module map=] is not [=map/empty=]:
1. Let |operationMap| be [=this=]'s {{SharedStorageWorkletGlobalScope}}'s [=SharedStorageWorkletGlobalScope/operation map=].
1. If |operationMap| [=map/contains=] |name|:
1. [=Assert=]: |operationMap|[|name|]'s [=associated realm=] is [=this=]'s [=relevant realm=].
1. Let |operation| be |operationMap|[|name|], [=converted to an IDL value|converted=] to {{Function}}.
1. Let |privateAggregationCompletionTask| be the result of [=setting up the Private Aggregation scopes=] given |workletDataOrigin|, |preSpecifiedParams| and |aggregationCoordinator|.
1. Let |argumentsList| be a new [=/list=].
1. If |options|["{{SharedStorageRunOperationMethodOptions/data}}"] [=map/exists=], [=list/append=] it to |argumentsList|.
1. [=Invoke=] |operation| with |argumentsList| and "`report`".
1. Wait for |operation| to finish running, if applicable.
1. Run |privateAggregationCompletionTask|.
1. If |options|["`keepAlive`"] is false:
1. Wait for |operation| to finish running, if applicable.
1. Run [=terminate a worklet global scope=] with [=this=].
</div>
<div algorithm>
To <dfn>obtain the aggregation coordinator</dfn> given a
{{SharedStorageRunOperationMethodOptions}} |options|, perform the following
steps. They return an [=aggregation coordinator=], null or a {{DOMException}}:
1. If |options|["{{SharedStorageRunOperationMethodOptions/privateAggregationConfig}}"]
does not [=map/exist=], return null.
1. If |options|["{{SharedStorageRunOperationMethodOptions/privateAggregationConfig}}"]["{{SharedStoragePrivateAggregationConfig/aggregationCoordinatorOrigin}}"]
does not [=map/exist=], return null.
1. Return the result of [=obtaining the Private Aggregation coordinator=]
given
|options|["{{SharedStorageRunOperationMethodOptions/privateAggregationConfig}}"]["{{SharedStoragePrivateAggregationConfig/aggregationCoordinatorOrigin}}"].
</div>
<div algorithm>
To <dfn>obtain the pre-specified report parameters</dfn> given a
{{SharedStorageRunOperationMethodOptions}} |options| and a [=/browsing
context=] |context|, perform the following steps. They return a
[=pre-specified report parameters=], null, or a {{DOMException}}:
1. If |options|["{{SharedStorageRunOperationMethodOptions/privateAggregationConfig}}"]
does not [=map/exist=], return null.
1. Let |privateAggregationConfig| be
|options|["{{SharedStorageRunOperationMethodOptions/privateAggregationConfig}}"].
1. Let |contextId| be null.
1. If |privateAggregationConfig|["{{SharedStoragePrivateAggregationConfig/contextId}}"]
[=map/exists=], set |contextId| to
|privateAggregationConfig|["{{SharedStoragePrivateAggregationConfig/contextId}}"].
1. If |contextId|'s [=string/length=] is greater than 64, return a new
{{DOMException}} with name "`DataError`".
1. Let |filteringIdMaxBytes| be the [=default filtering ID max bytes=].
1. If |privateAggregationConfig|["{{SharedStoragePrivateAggregationConfig/filteringIdMaxBytes}}"]
[=map/exists=], set |filteringIdMaxBytes| to
|privateAggregationConfig|["{{SharedStoragePrivateAggregationConfig/filteringIdMaxBytes}}"].
1. If |filteringIdMaxBytes| is not [=set/contained=] in the [=valid filtering ID
max bytes range=], return a new {{DOMException}} with name "`DataError`".
1. If |context|'s [=browsing context/fenced frame config instance=] is not null:
1. If |filteringIdMaxBytes| is not the [=default filtering ID max bytes=] or
|contextId| is not null, return a new {{DOMException}} with name
"`DataError`".
1. Return a new [=pre-specified report parameters=] with the items:
: <a spec="private-aggregation-api" for="pre-specified report parameters">context ID</a>
:: |contextId|
: [=pre-specified report parameters/filtering ID max bytes=]
:: |filteringIdMaxBytes|
</div>
<div algorithm>
To <dfn>set up the Private Aggregation scopes</dfn> given an [=/origin=]
|workletDataOrigin|, a [=pre-specified report parameters=] or null
|preSpecifiedParams| and an [=aggregation coordinator=] or null
|aggregationCoordinator|, peform the following steps. They return an
algorithm.
Note: The returned algorithm should be run when the associated operation is
complete.
1. Let |batchingScope| be a new [=batching scope=].
1. Let |debugScope| be a new [=debug scope=].
1. Let |privateAggregationTimeout| be null.
1. Let |hasRunPrivateAggregationCompletionTask| be false.
1. Let |privateAggregationCompletionTask| be an algorithm to perform the
following steps:
1. If |hasRunPrivateAggregationCompletionTask|, return.
1. Set |hasRunPrivateAggregationCompletionTask| to true.
1. [=Mark a debug scope complete=] given |debugScope|.
1. [=Process contributions for a batching scope=] given
|batchingScope|, |workletDataOrigin|, "<code>shared-storage</code>"
and |privateAggregationTimeout|.
1. If |aggregationCoordinator| is not null, [=set the aggregation coordinator
for a batching scope=] given |aggregationCoordinator| and |batchingScope|.
1. If |preSpecifiedParams| is not null:
1. Let |isDeterministicReport| be the result of [=determining if a report
should be sent deterministically=] given |preSpecifiedParams|.
1. If |isDeterministicReport|:
1. Set |privateAggregationTimeout| to the [=/current wall time=] plus
the [=deterministic operation timeout duration=].
1. [=Set the pre-specified report parameters for a batching scope=] given
|preSpecifiedParams| and |batchingScope|.
1. If |isDeterministicReport|, run the following steps [=in parallel=]:
1. Wait until |privateAggregationTimeout|.
1. Run |privateAggregationCompletionTask|.
1. Return |privateAggregationCompletionTask|.
</div>
The <dfn>deterministic operation timeout duration</dfn> is an
[=implementation-defined=] non-negative [=duration=] that controls how long a
Shared Storage operation may make Private Aggregation contributions if it is
triggering a deterministic report and, equivalently, when that report should
be sent after the operation begins.
## Monkey Patch for [=Worklets=] ## {#worklet-monkey-patch}
This specification will make some modifications to the [=Worklet=] standard to accommodate the needs of Shared Storage.
### Monkey Patch for [=set up a worklet environment settings object=] ### {#set-up-a-worklet-environment-settings-object-monkey-patch}
The [=set up a worklet environment settings object=] algorithm will need to include an additional parameter: {{Worklet}} |worklet|. The step that defines the |settingsObject|'s [=environment settings object/origin=] should be modified as follows:
6. Let |settingsObject| be a new [=environment settings object=] whose algorithms are defined as follows:
......
<b>The [=environment settings object/origin=]</b>
1. Let |workletGlobalScope| be the [=global object=] of <var ignore=''>realmExecutionContext</var>'s Realm component.
1. If |workletGlobalScope| is not {{SharedStorageWorkletGlobalScope}}, return |origin|.
1. [=Assert=] that |worklet| is a {{SharedStorageWorklet}}.
1. If |worklet|'s [=SharedStorageWorklet/data origin=] is `"context-origin"`, return <var ignore=''>outsideSettings</var>'s [=environment settings object/origin=].
1. Otherwise, if [=SharedStorageWorklet/data origin=] is `"script-origin"`:
1. Let |pendingAddedModules| be a [=list/clone=] of |worklet|'s [=added modules list=].
1. [=Assert=]: |pendingAddedModules|'s [=list/size=] is 1.
1. Let |moduleURL| be |pendingAddedModules|[0].
1. Return |moduleURL|'s [=url/origin=].
1. Otherwise, let |customOriginUrl| be the result of running a [=URL parser=] on [=SharedStorageWorklet/data origin=].
1. [=Assert=] |customOriginUrl| is a valid [=/URL=].
1. Return |customOriginUrl|'s [=url/origin=].
......
### Monkey Patch for [=create a worklet global scope=] ### {#create-a-worklet-global-scope-monkey-patch}
The [=create a worklet global scope=] algorithm will need to be modified to pass in the |worklet| parameter:
5. Let <var ignore=''>insideSettings</var> be the result of [=setting up a worklet environment settings object=] given <var ignore=''>realmExecutionContext</var>, <var ignore=''>outsideSettings</var>, and |worklet|.
### Monkey Patch for [=fetch a worklet script graph=] ### {#fetch-a-worklet-script-graph-monkey-patch}
The algorithm [=fetch a worklet script graph=] calls into the <a href="https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-worklet/module-worker-script-graph">fetch a worklet/module worker script graph</a> algorithm, which takes in an algorithm parameter |processCustomFetchResponse|. The definition of that |processCustomFetchResponse| parameter will need to include the following step before the step "5. [=Fetch=] |request|, ...":
5. If |fetchClient|'s [=environment settings object/global object=] is {{SharedStorageWorkletGlobalScope}}:
1. Set |request|'s [=request/redirect mode=] to "<code>error</code>".
Note: For shared storage, redirects are disallowed for the module script request. With this restriction, it's possible to define and to use the algorithm that gets the |realm|'s [=realm/settings object=]'s [=environment settings object/origin=] (as described in [[#set-up-a-worklet-environment-settings-object-monkey-patch]]) as soon as the {{SharedStorageWorkletGlobalScope}} is created, as the origin won't change. This restriction may be removed in a future iteration of the design. If redirects become allowed, presumably, the algorithm that gets the |realm|'s [=realm/settings object=]'s [=environment settings object/origin=] should be updated to return the final request's [=request/URL=]'s [=url/origin=] after receiving the final request's response, and the user preference checkings shall only be done after that point.
1. If |fetchClient|'s [=environment settings object/origin=] and |settingsObject|'s [=environment settings object/origin=] are not [=same origin=]:
1. Let |dataOriginValue| be the [=origin/serialization=] of |settingsObject|'s [=environment settings object/origin=].
1. [=Assert=] that |dataOriginValue| is not null.
1. [=header list/Append=] the [=header=] ([:Sec-Shared-Storage-Data-Origin:], |dataOriginValue|) to |request|'s [=request/header list=].
### The [:Shared-Storage-Cross-Origin-Worklet-Allowed:] HTTP response header ### {#worklet-allowed-header}
The [:Shared-Storage-Cross-Origin-Worklet-Allowed:] HTTP response header, along with the traditional CORS headers, can be used to grant a cross-origin site the permission to create a worklet from the module script's [=/URL=]'s [=url/origin=], and to run subsequent operations on the worklet using the module script's [=/URL=]'s [=url/origin=] as the <dfn for="SharedStorage">data partition origin</dfn> for accessing shared storage data, i.e. the [=environment settings object/origin=] set in [[#set-up-a-worklet-environment-settings-object-monkey-patch]], which becomes the [=url/origin=] used in all {{SharedStorage}} calls to [=obtain a shared storage bottle map=].
Worklets that load cross-origin scripts rely on CORS as a baseline permission mechanism to indicate trusted external origins. However, CORS alone is insufficient for creation of a worklet with cross-origin script whose [=data partition origin=] is the script origin. Unlike simple resource sharing, worklets allow the creator site to execute JavaScript within the context of the target origin. To ensure security, an additional response header, [:Shared-Storage-Cross-Origin-Worklet-Allowed:], is required from the script origin.
### Monkey Patch for [=HTTP fetch=] ### {#http-fetch-monkey-patch}
[Steps](#mod-http-fetch) will need to be added to the [=HTTP fetch=] algorithm.
Note: It is the responsibility of the site serving the module script to carefully consider the security implications: when the module script's [=/URL=]'s [=url/origin=] and the worklet's creator {{Window}} origin are not [=same origin=], by sending permissive CORS headers the [:Shared-Storage-Cross-Origin-Worklet-Allowed:] header on the module script response, the server will be granting the worklet's creation and subsequent operations on the worklet, while allowing the worklet to use the worklet's script's [=url/origin=] as the [=url/origin=] for accessing the shared storage data, i.e. the [=data partition origin=]. For example, the worklet's creator {{Window}} could poison and use up the worklet origin's [=/site=]'s [=site/remaining navigation budget=] by calling {{SharedStorageWorklet/selectURL()}} or {{SharedStorageWorklet/run()}}, where the worklet origin is the global scope's [=global object/realm=]'s [=realm/settings object=]'s [=environment settings object/origin=].
### Monkey Patch for {{Worklet/addModule()}} ### {#add-module-monkey-patch}
The {{Worklet/addModule()}} method steps for {{Worklet}} will need to include the following step before the step "Let |promise| be a new promise":
4. If |this| is of type {{SharedStorageWorklet}}:
1. Let |addModuleAllowedResult| be the result of running [=check if addModule is allowed and update state=] given |this| and <var ignore=''>moduleURLRecord</var>.
1. If |addModuleAllowedResult| is "DisallowedDueToNonPreferenceError":
1. Return [=a promise rejected with=] a {{TypeError}}.
1. Else if |addModuleAllowedResult| is "DisallowedDueToPreferenceError":
1. If |this|'s [=SharedStorageWorklet/has cross-origin data origin=] is false, then return [=a promise rejected with=] a {{TypeError}}.
1. Else:
1. [=Assert=]: |addModuleAllowedResult| is "Allowed".
<div class="note">
On user preferences error, {{Worklet/addModule()}} will be aborted at an early stage. However, the error will only be exposed to the caller for a same-origin worklet (i.e. where the initiator document's origin is same-origin with the module script's origin). For a cross-origin worklet, the error will be hidden. This is to prevent a caller from knowing which origins the user has disabled shared storage for via preferences (if a per-origin preference exists for that browser vendor).
A caller may still use timing attacks to know this information, but this is a minor security/privacy issue, as in reality very few users would set such preferences, and doing a wide search would incur a significant performance cost spinning up the worklets.
This rationale also applies to the handling for user preferences error for {{SharedStorageWorklet/selectURL()}} and {{SharedStorageWorklet/run()}}.
</div>
After the step "Let <var ignore=''>addedSuccessfully</var> be false", we need to include the following step:
4. If |this| is of type {{SharedStorageWorklet}}, [=SharedStorageWorklet/has cross-origin data origin=] is true, and [=SharedStorageWorklet/data origin=] is not `"script-origin"`:
1. [=Assert=] |pendingTasks| is 1.
1. Set |pendingTasks| to 2.
1. [=Queue a global task=] on the [=networking task source=] given <var ignore=''>workletGlobalScope</var> to perform the following steps:
1. Let |customOriginUrl| be the result of running a [=URL parser=] on [=SharedStorageWorklet/data origin=].
1. [=Assert=] |customOriginUrl| is a valid [=/URL=].
1. Set |customOriginUrl|'s [=url/path=] to ≪".well-known", "shared-storage", "trusted-origins"≫.
1. Let |request| be a new [=/request=] whose [=request/URL=] is |customOriginUrl|, [=request/mode=] is `"cors"`, [=request/referrer=] is `"client"`, [=request/destination=] is `"json"`, [=request/initiator type=] is `"script"`, and [=request/client=] is |outsideSettings|.
1. [=Fetch=] |request| with [=fetch/processResponseConsumeBody=] set to the following algorithm, given [=/response=] |response| and null, failure or a [=/byte sequence=] |bodyBytes|:
1. If any of the following are true:
* |bodyBytes| is null or failure; or
* |response|'s [=response/status=] is not an [=ok status=],
then:
1. Set |pendingTasks| to −1.
1. [=Reject=] |promise| with an "TypeError" DOMException.
1. Abort these steps.
1. Let |mimeType| be the result of [=extracting a MIME type=] from |response|'s [=response/header list=].
1. If |mimeType| is not a [=JSON MIME type=], then:
1. Set |pendingTasks| to −1.
1. [=Reject=] |promise| with an "TypeError" DOMException.
1. Abort these steps.
1. Let |sourceText| be the result of [=UTF-8 decoding=] |bodyBytes|.
1. Let |parsed| be the result of [=parsing a JSON string to an Infra value=] given |sourceText|.
1. If |parsed| is not a [=list=] or if |parsed| is [=list/empty=], then:
1. Set |pendingTasks| to −1.
1. [=Reject=] |promise| with an "TypeError" DOMException.
1. Abort these steps.
1. Let |doesMatch| be false.
1. For each |item| of |parsed|:
1. If |item| is not an [=ordered map=], or if |item| does not [=map/contain=] `scriptOrigin`, or if |item| does not [=map/contain=] `contextOrigin`:
1. Set |pendingTasks| to −1.
1. [=Reject=] |promise| with an "TypeError" DOMException.
1. Abort these steps.
1. Let |doesMatch| be the result of running [=check for script and context origin match=] on |item|[`scriptOrigin`], <var ignore=''>moduleURLRecord</var>'s [=url/origin=], |item|[`contextOrigin`], and |outsideSettings|'s [=environment settings object/origin=].
1. If |doesMatch| is true:
1. [=Queue a global task=] on the [=networking task source=] given |this|'s [=relevant global object=] to perform the following steps:
1. If |pendingTasks| is not −1, then:
1. Set |pendingTasks| to |pendingTasks| − 1.
1. If |pendingTasks| is 0, perform the following steps:
1. If |workletGlobalScope| has an associated boolean [=addModule success=], set |workletGlobalScope|'s [=addModule success=] to true.
1. [=Resolve=] |promise|.
1. Break.
1. If |doesMatch| is false, then:
1. Set |pendingTasks| to −1.
1. [=Reject=] |promise| with an "TypeError" DOMException.
Note: If the worklet data origin is different from the current context and the script origin, an additional check is performed. This involves fetching a configuration file from the worklet data origin to verify that the current context is allowed to load the worklet with the script and perform operations.
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|.
Just before the final step, currently "Return <var ignore>promise</var>.", add the following step:
7. If |this| is a {{SharedStorageWorklet}}, [=upon fulfillment=] of |promise| or
[=upon rejection=] of |promise|, run the following steps:
1. Let |globalScopes| be |this|'s [=Worklet/global scopes=].
1. [=Assert=]: |globalScopes|' [=list/size=] equals 1.
1. Let |privateAggregationObj| be |globalScopes|[0]'s
{{SharedStorageWorkletGlobalScope/privateAggregation}}.
1. Set |privateAggregationObj|'s [=PrivateAggregation/allowed to use=] to
the result of determining whether [=this=]'s [=relevant global
object=]'s [=associated document=] is [=/allowed to use=] the
"<code>[=private-aggregation=]</code>" [=policy-controlled feature=].
Issue: Consider adding an early return here if the permissions
policy check is made first.
1. Set |privateAggregationObj|'s [=PrivateAggregation/scoping details=] to a
new [=/scoping details=] with the items:
: [=scoping details/get batching scope steps=]
:: An algorithm that returns the [=batching scope=] that is scheduled to
be passed to [=process contributions for a batching scope=] when the
call currently executing in |scope| returns.
: [=scoping details/get debug scope steps=]
:: An algorithm that returns the [=debug scope=] that is scheduled to be
passed to [=mark a debug scope complete=] when the call currently
executing in |scope| returns.
Note: Multiple operation invocations can be in-progress at the same
time, each with a different batching scope and debug scope. However,
only one can be currently executing.
A <dfn>trusted origin type</dfn> is a [=string=] or [=list=] of [=strings=].
<div algorithm>
To <dfn>check for script and context origin match</dfn>, given [=trusted origin type=] |itemScriptOrigin|, [=url/origin=] |actualScriptOrigin|, [=trusted origin type=] |itemContextOrigin|, and [=environment settings object/origin=] |actualContextOrigin|, peform the following steps:
1. If the result of running [=check for trusted origin match=], given |itemScriptOrigin| and |actualScriptOrigin| is false, return false.
1. Return the result of running [=check for trusted origin match=], given |itemContextOrigin| and |actualContextOrigin|.
</div>
<div algorithm>
To <dfn>check for trusted origin match</dfn>, given [=trusted origin type=] |itemOrigin| and [=url/origin=] |actualOrigin|, peform the following steps:
1. If |itemOrigin| is a [=string=], return the result of running [=check for trusted origin match on a string=], given |itemOrigin| and |actualOrigin|.
1. Otherwise, for each |originString| in |itemOrigin|:
1. If the result of running [=check for trusted origin match on a string=] given |originString| and |actualOrigin| is true, return true.
1. Return false.
</div>
<div algorithm>
To <dfn>check for trusted origin match on a string</dfn>, given [=string=] |itemOrigin| and [=url/origin=] |actualOrigin|, peform the following steps:
1. If |itemOrigin| is `"*"`, return true.
1. Let |itemOriginUrl| be the result of running a [=URL parser=] on |itemOrigin|.
1. If |itemOriginUrl| is not a valid [=/URL=], then return false.
1. If |itemOriginUrl|'s [=url/origin=] and |actualOrigin| are [=same origin=], return true.
1. Otherwise, return false.
</div>
<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}}.
The {{SharedStorageWorklet}}'s [=worklet destination type=] is "sharedstorageworklet".
### Monkey Patch for request [=request/destination=] ### {#request-destination-monkey-patch}
The fetch request's [=request/destination=] field should additionally include "sharedstorageworklet" as a valid value.
<xmp class='idl'>
callback RunFunctionForSharedStorageSelectURLOperation = Promise<unsigned long>(sequence<USVString> urls, optional any data);
</xmp>
<xmp class='idl'>
[Exposed=SharedStorageWorklet, Global=SharedStorageWorklet]
interface SharedStorageWorkletGlobalScope : WorkletGlobalScope {
undefined register(DOMString name,
Function operationCtor);
readonly attribute SharedStorage sharedStorage;
readonly attribute PrivateAggregation privateAggregation;
Promise<sequence<StorageInterestGroup>> interestGroups();
};
</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.
Each {{SharedStorageWorkletGlobalScope}} also has an associated <dfn for=SharedStorageWorkletGlobalScope>operation map</dfn>, which is a [=map=], initially empty, of [=strings=] (denoting operation names) to [=function objects=].
Each {{SharedStorageWorkletGlobalScope}} also has an associated {{SharedStorage}} 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. Let |operationClassInstance| be the result of [=constructing=] |operationCtor|, with no arguments.
1. Let |runFunction| be [=Get=](|operationClassInstance|, "`run`"). Rethrow any exceptions.
1. If <a abstract-op>IsCallable</a>(|runFunction|) is false, throw a {{TypeError}}.
1. [=map/Set=] the value of |operationMap|[|name|] to |runFunction|.
</div>
Issue(151): The "name" and "operationCtor" cannot be missing here given WebIDL. Should just check for default/empty values.
<div algorithm>
The <dfn method for="SharedStorageWorkletGlobalScope">interestGroups()</dfn> method steps are:
1. Let |promise| be a new [=promise=].
1. If the result of running [=SharedStorageWorkletGlobalScope/check whether addModule is finished=] for {{SharedStorage}}'s associated {{SharedStorageWorkletGlobalScope}} is false, return a [=promise rejected=] with a {{TypeError}}.
1. Let |globalObject| be the [=current realm=]'s [=global object=].
1. Let |context| be |globalObject|'s [=Window/browsing context=].
1. If |context| is null, return a [=promise rejected=] with a {{TypeError}}.
1. Let |document| be |context|'s [=active window=]'s [=associated document=].
1. If |document| is not [=fully active=], return a [=promise rejected=] with a {{TypeError}}.
1. Let |workletDataOrigin| be [=current realm=]'s [=realm/settings object=]'s [=environment settings object/origin=].
1. Run the following steps [=in parallel=]:
1. Let |interestGroups| be the result of running [=get storage interest groups for owner=] given |workletDataOrigin|.
1. If |interestGroups| is failure:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to [=reject=] |promise| with a {{TypeError}}.
1. Otherwise:
1. [=Queue a global task=] on the [=DOM manipulation task source=], given |realm|'s [=global object=], to [=resolve=] |promise| with |interestGroups|.
1. Return |promise|.
</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="privateAggregation getter">
The {{SharedStorageWorkletGlobalScope/privateAggregation}} [=getter steps=] are to:
1. [=Get the privateAggregation=] given [=this=].
</div>
<div algorithm>
To <dfn for="SharedStorageWorkletGlobalScope">check whether addModule is finished</dfn>, the step is:
1. Return the value of [=addModule success=].
</div>
## {{SharedStorageUrlWithMetadata}} and Reporting ## {#reporting}
<xmp class='idl'>
dictionary SharedStorageUrlWithMetadata {
required USVString url;
object reportingMetadata;
};
</xmp>
If a {{SharedStorageUrlWithMetadata}} [=dictionary=] contains a non-[=map/empty=] {{SharedStorageUrlWithMetadata/reportingMetadata}} {{/object}} in the form of a [=dictionary=] whose [=map/keys=] are {{FenceEvent}}'s {{FenceEvent/eventType}}s 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 {{SharedStorageWorklet/selectURL()}} call.
Issue(141): {{SharedStorageUrlWithMetadata/reportingMetadata}} should be a [=dictionary=].
Inside a [=fenced frame=] with {{FenceEvent/eventType}}-[=/URL=] pairs that have been [=register reporting metadata|registered=] through {{SharedStorageWorklet/selectURL()}} with {{SharedStorageUrlWithMetadata/reportingMetadata}} {{/object}}s, if {{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 not a [=dictionary=], return false.
1. If |reportingMetadata| is [=map/empty=], return true.
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=] |fencedFrameConfigStruct|, run the following steps:
1. If |reportingMetadata| is [=map/empty=], return.
1. [=Assert=]: |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=]: |url| is a valid [=/URL=].
1. [=map/Set=] |reportingUrlMap|[|eventType|] to |url|.
Issue(144): Store |reportingUrlMap| inside a [=fenced frame reporter=] class associated with |fencedFrameConfigStruct|. Both of these still need to be added to the draft [[Fenced-Frame]].
</div>
## Entropy Budgets ## {#budgets}
Because [=bits of entropy=] can leak via {{SharedStorageWorklet/selectURL()}}, the [=user agent=] will need to maintain budgets to limit these leaks.
On a call to {{SharedStorageWorklet/selectURL()}}, when any of these budgets are exhausted, the [=default selectURL index=] will be used to determine which URL to select.
<div algorithm>
To <dfn>get the default selectURL index</dfn>, given {{sequence}}<{{USVString}}> |urls|, run the following steps:
1. Return 0.
Note: We could have chosen to return any {{unsigned long}} from the [=the exclusive range|range=] from 0 to |urls|’s [=list/size=], exclusive, as long as the returned index was independent from the registered operation class's "`run`" method.
</div>
The <dfn>default selectURL index</dfn> is the index obtained by running [=get the default selectURL index=], given {{sequence}}<{{USVString}}> <var ignore=''>urls</var>.
### Navigation Entropy Budget ### {#nav-budget}
If a user [=user activation|activates=] a [=fenced frame=] whose [=Node/node document=]'s [=Document/browsing context=]'s [=browsing context/fenced frame config instance=] was generated by {{SharedStorageWorklet/selectURL()}} and thereby initiates a [=top-level traversable=] [=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 {{SharedStorageWorklet/selectURL()}}. To mitigate this, a [=user agent=] will set a per-[=/site=] [=navigation entropy allowance=].
A <dfn>navigation entropy allowance</dfn> is a maximum allowance of [=entropy bits=] that are permitted to leak via [=fenced frames=] initiating [=top-level traversable=] [=navigate|navigations=] during a given [=navigation budget epoch=] for a given calling [=/site=]. This [=navigation entropy allowance|allowance=] is defined by the [=user agent=] and is [=/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 [=/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=] with the following [=struct/items=]:
<dl dfn-for="bit debit">
: <dfn>bits</dfn>
:: a {{double}}
: <dfn>timestamp</dfn>
:: a {{DOMHighResTimeStamp}} (from the [=Unix Epoch=])
</dl>
[=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>.