@peax/consent-core 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +207 -0
- package/dist/ConsentEnforcementError-Bn_ayWJI.js +16 -0
- package/dist/assertStorageIdentifier-BQuj-9bp.js +7 -0
- package/dist/createConsentIframe-Bw-cl--m.js +93 -0
- package/dist/createConsentRequestScope-D7U8uK6m.js +58 -0
- package/dist/createConsentScript-C_4ecSTX.js +102 -0
- package/dist/createDomGuard-CuIEC0HP.js +101 -0
- package/dist/enforcement/axios.d.ts +222 -0
- package/dist/enforcement/axios.js +92 -0
- package/dist/enforcement/fetch.d.ts +222 -0
- package/dist/enforcement/fetch.js +40 -0
- package/dist/enforcement/iframe.d.ts +245 -0
- package/dist/enforcement/iframe.js +2 -0
- package/dist/enforcement/integration.d.ts +230 -0
- package/dist/enforcement/integration.js +181 -0
- package/dist/enforcement/markup.d.ts +214 -0
- package/dist/enforcement/markup.js +300 -0
- package/dist/enforcement/posthog.d.ts +68 -0
- package/dist/enforcement/posthog.js +96 -0
- package/dist/enforcement/request-rules.d.ts +248 -0
- package/dist/enforcement/request-rules.js +434 -0
- package/dist/enforcement/script.d.ts +240 -0
- package/dist/enforcement/script.js +2 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +445 -0
- package/dist/storage/localforage.d.ts +23 -0
- package/dist/storage/localforage.js +26 -0
- package/dist/storage/sync.d.ts +278 -0
- package/dist/storage/sync.js +370 -0
- package/dist/storage.d.ts +77 -0
- package/dist/storage.js +207 -0
- package/dist/validate-D16G7mdG.js +15 -0
- package/package.json +128 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
|
|
3
|
+
/** Disjoint category/use decisions for one scope actually presented to the user. */
|
|
4
|
+
declare type BatchCommand = z.infer<typeof consentDecisionsCommandSchema>;
|
|
5
|
+
|
|
6
|
+
/** Records choices only for category members in userConsentedPolicy, not future members. */
|
|
7
|
+
declare type CategoryConsentCommand = z.infer<typeof categoryConsentCommandSchema>;
|
|
8
|
+
|
|
9
|
+
declare const categoryConsentCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
10
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
11
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
12
|
+
id: z.ZodMiniString<string>;
|
|
13
|
+
title: z.ZodMiniString<string>;
|
|
14
|
+
description: z.ZodMiniString<string>;
|
|
15
|
+
}, z.core.$strict>>>>;
|
|
16
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
17
|
+
id: z.ZodMiniString<string>;
|
|
18
|
+
serviceId: z.ZodMiniString<string>;
|
|
19
|
+
categoryId: z.ZodMiniString<string>;
|
|
20
|
+
purposeId: z.ZodMiniString<string>;
|
|
21
|
+
consentVersion: z.ZodMiniInt;
|
|
22
|
+
}, z.core.$strict>>>>;
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
|
+
categoryId: z.ZodMiniString<string>;
|
|
25
|
+
decision: z.ZodMiniEnum<{
|
|
26
|
+
granted: "granted";
|
|
27
|
+
denied: "denied";
|
|
28
|
+
}>;
|
|
29
|
+
}, z.core.$strict>>;
|
|
30
|
+
|
|
31
|
+
/** userConsentedPolicy is the scope shown for this decision, including a denial. */
|
|
32
|
+
declare type ConsentDecisionCommand = z.infer<typeof consentDecisionCommandSchema>;
|
|
33
|
+
|
|
34
|
+
declare const consentDecisionCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
35
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
36
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
37
|
+
id: z.ZodMiniString<string>;
|
|
38
|
+
title: z.ZodMiniString<string>;
|
|
39
|
+
description: z.ZodMiniString<string>;
|
|
40
|
+
}, z.core.$strict>>>>;
|
|
41
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
42
|
+
id: z.ZodMiniString<string>;
|
|
43
|
+
serviceId: z.ZodMiniString<string>;
|
|
44
|
+
categoryId: z.ZodMiniString<string>;
|
|
45
|
+
purposeId: z.ZodMiniString<string>;
|
|
46
|
+
consentVersion: z.ZodMiniInt;
|
|
47
|
+
}, z.core.$strict>>>>;
|
|
48
|
+
}, z.core.$strict>>;
|
|
49
|
+
useId: z.ZodMiniString<string>;
|
|
50
|
+
decision: z.ZodMiniEnum<{
|
|
51
|
+
granted: "granted";
|
|
52
|
+
denied: "denied";
|
|
53
|
+
}>;
|
|
54
|
+
}, z.core.$strict>>;
|
|
55
|
+
|
|
56
|
+
declare type ConsentDecisionsCommand = Readonly<Omit<BatchCommand, 'decisions'> & {
|
|
57
|
+
decisions: readonly ExclusiveTarget<BatchCommand['decisions'][number]>[];
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
declare const consentDecisionsCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
61
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
62
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
63
|
+
id: z.ZodMiniString<string>;
|
|
64
|
+
title: z.ZodMiniString<string>;
|
|
65
|
+
description: z.ZodMiniString<string>;
|
|
66
|
+
}, z.core.$strict>>>>;
|
|
67
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
68
|
+
id: z.ZodMiniString<string>;
|
|
69
|
+
serviceId: z.ZodMiniString<string>;
|
|
70
|
+
categoryId: z.ZodMiniString<string>;
|
|
71
|
+
purposeId: z.ZodMiniString<string>;
|
|
72
|
+
consentVersion: z.ZodMiniInt;
|
|
73
|
+
}, z.core.$strict>>>>;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
decisions: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniUnion<readonly [z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
76
|
+
useId: z.ZodMiniString<string>;
|
|
77
|
+
decision: z.ZodMiniEnum<{
|
|
78
|
+
granted: "granted";
|
|
79
|
+
denied: "denied";
|
|
80
|
+
}>;
|
|
81
|
+
}, z.core.$strict>>, z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
82
|
+
categoryId: z.ZodMiniString<string>;
|
|
83
|
+
decision: z.ZodMiniEnum<{
|
|
84
|
+
granted: "granted";
|
|
85
|
+
denied: "denied";
|
|
86
|
+
}>;
|
|
87
|
+
}, z.core.$strict>>]>>>;
|
|
88
|
+
}, z.core.$strict>>;
|
|
89
|
+
|
|
90
|
+
declare type ConsentDeniedReason = 'unknown-use' | 'undecided' | 'denied' | 'consent-version-mismatch';
|
|
91
|
+
|
|
92
|
+
declare type ConsentEvaluation = Readonly<{
|
|
93
|
+
allowed: true;
|
|
94
|
+
}> | Readonly<{
|
|
95
|
+
allowed: false;
|
|
96
|
+
reason: ConsentDeniedReason;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
declare type ConsentPolicy = z.infer<typeof consentPolicySchema>;
|
|
100
|
+
|
|
101
|
+
declare const consentPolicySchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
102
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
103
|
+
id: z.ZodMiniString<string>;
|
|
104
|
+
title: z.ZodMiniString<string>;
|
|
105
|
+
description: z.ZodMiniString<string>;
|
|
106
|
+
}, z.core.$strict>>>>;
|
|
107
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
108
|
+
id: z.ZodMiniString<string>;
|
|
109
|
+
serviceId: z.ZodMiniString<string>;
|
|
110
|
+
categoryId: z.ZodMiniString<string>;
|
|
111
|
+
purposeId: z.ZodMiniString<string>;
|
|
112
|
+
consentVersion: z.ZodMiniInt;
|
|
113
|
+
}, z.core.$strict>>>>;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
|
|
116
|
+
/** A current use with no decision or a grant for a different consent version. */
|
|
117
|
+
declare type ConsentRequirement = Readonly<{
|
|
118
|
+
useId: string;
|
|
119
|
+
categoryId: string;
|
|
120
|
+
/** The version required by the current policy for this use. */
|
|
121
|
+
consentVersion: number;
|
|
122
|
+
reason: 'undecided' | 'consent-version-mismatch';
|
|
123
|
+
}>;
|
|
124
|
+
|
|
125
|
+
/** In-memory runtime; methods can be passed as callbacks without binding. */
|
|
126
|
+
declare type ConsentRuntime = Readonly<{
|
|
127
|
+
getSnapshot: () => ConsentSnapshot;
|
|
128
|
+
evaluate: (useId: string) => ConsentEvaluation;
|
|
129
|
+
/** Includes undecided uses and version-mismatched grants, not explicit denials. */
|
|
130
|
+
getMismatchedConsent: () => readonly ConsentRequirement[];
|
|
131
|
+
/**
|
|
132
|
+
* Replace all choices from trusted persistence, preserving unknown uses and recorded versions.
|
|
133
|
+
* Validation checks structure, not evidence of user consent; use decision methods for new choices.
|
|
134
|
+
* Does not replace policy, renew versions or perform I/O. Effective changes notify existing guards.
|
|
135
|
+
*/
|
|
136
|
+
restoreState: (state: unknown) => ValidationResult<ConsentSnapshot>;
|
|
137
|
+
setConsentDecision: (command: ConsentDecisionCommand) => ValidationResult<ConsentSnapshot>;
|
|
138
|
+
setCategoryConsent: (command: CategoryConsentCommand) => ValidationResult<ConsentSnapshot>;
|
|
139
|
+
/** Validates all targets before one commit; observer errors can throw after that commit. */
|
|
140
|
+
setConsentDecisions: (command: ConsentDecisionsCommand) => ValidationResult<ConsentSnapshot>;
|
|
141
|
+
updatePolicy: (currentAppPolicy: unknown) => ValidationResult<ConsentSnapshot>;
|
|
142
|
+
subscribe: (listener: () => void) => () => void;
|
|
143
|
+
}>;
|
|
144
|
+
|
|
145
|
+
/** One active app policy and recorded choices; not a history of policies shown to users. */
|
|
146
|
+
declare type ConsentSnapshot = Readonly<{
|
|
147
|
+
currentAppPolicy: ConsentPolicy;
|
|
148
|
+
state: ConsentState;
|
|
149
|
+
}>;
|
|
150
|
+
|
|
151
|
+
declare type ConsentState = z.infer<typeof consentStateSchema>;
|
|
152
|
+
|
|
153
|
+
declare const consentStateSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
154
|
+
choices: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
155
|
+
useId: z.ZodMiniString<string>;
|
|
156
|
+
consentVersion: z.ZodMiniInt;
|
|
157
|
+
decision: z.ZodMiniEnum<{
|
|
158
|
+
granted: "granted";
|
|
159
|
+
denied: "denied";
|
|
160
|
+
}>;
|
|
161
|
+
}, z.core.$strict>>>>;
|
|
162
|
+
}, z.core.$strict>>;
|
|
163
|
+
|
|
164
|
+
declare type ConsentStorage = Readonly<{
|
|
165
|
+
/** Null means no record. Invalid or unreadable records return failure, never empty choices. */
|
|
166
|
+
load: () => Promise<ConsentStorageResult<ConsentState | null>>;
|
|
167
|
+
/** Captures validated state at invocation; writes run in invocation order. */
|
|
168
|
+
save: (state: unknown) => Promise<ConsentStorageResult<void>>;
|
|
169
|
+
/** Available only when the adapter supports external record invalidation. Does not load or save. */
|
|
170
|
+
subscribe?: (listener: () => void) => () => void;
|
|
171
|
+
}>;
|
|
172
|
+
|
|
173
|
+
/** Storage failures deliberately exclude raw errors, URLs, headers, and stored values. */
|
|
174
|
+
declare type ConsentStorageError = Readonly<{
|
|
175
|
+
type: 'read' | 'write';
|
|
176
|
+
}> | Readonly<{
|
|
177
|
+
type: 'validation';
|
|
178
|
+
issues: readonly ValidationIssue[];
|
|
179
|
+
}>;
|
|
180
|
+
|
|
181
|
+
declare type ConsentStorageResult<T> = Readonly<{
|
|
182
|
+
success: true;
|
|
183
|
+
data: T;
|
|
184
|
+
}> | Readonly<{
|
|
185
|
+
success: false;
|
|
186
|
+
error: ConsentStorageError;
|
|
187
|
+
}>;
|
|
188
|
+
|
|
189
|
+
export declare type ConsentSync = Readonly<{
|
|
190
|
+
/** Claim this runtime, open the channel and refresh. Another started owner returns runtime-in-use. */
|
|
191
|
+
start: () => Promise<ConsentSyncResult>;
|
|
192
|
+
/**
|
|
193
|
+
* Call after saving the exact state object captured from consent.getSnapshot().state.
|
|
194
|
+
* Clones/revalidated copies and older snapshots broadcast invalidation but do not acknowledge pending choices.
|
|
195
|
+
* Success confirms sending, not acknowledgment or delivery. Does not save or broadcast choices.
|
|
196
|
+
*/
|
|
197
|
+
notify: (savedState: ConsentState) => ConsentSyncResult;
|
|
198
|
+
/** Share same-turn work; later in-flight invalidations require a fresh trailing read before settlement. */
|
|
199
|
+
refresh: () => Promise<ConsentSyncResult>;
|
|
200
|
+
getSnapshot: () => ConsentSyncSnapshot;
|
|
201
|
+
/** Listener failures are contained after commit; registrations remain until unsubscribed or disposed. */
|
|
202
|
+
subscribe: (listener: () => void) => () => void;
|
|
203
|
+
/** Close owned listeners/channel; pending storage I/O is not aborted but can no longer apply. */
|
|
204
|
+
dispose: () => ConsentSyncResult;
|
|
205
|
+
}>;
|
|
206
|
+
|
|
207
|
+
/** Structural BroadcastChannel contract for application-configured transports and tests. */
|
|
208
|
+
export declare type ConsentSyncChannel = Readonly<{
|
|
209
|
+
postMessage: (message: unknown) => void;
|
|
210
|
+
addEventListener: (type: 'message', listener: (event: Readonly<{
|
|
211
|
+
data: unknown;
|
|
212
|
+
}>) => void) => void;
|
|
213
|
+
removeEventListener: (type: 'message', listener: (event: Readonly<{
|
|
214
|
+
data: unknown;
|
|
215
|
+
}>) => void) => void;
|
|
216
|
+
close: () => void;
|
|
217
|
+
}>;
|
|
218
|
+
|
|
219
|
+
export declare type ConsentSyncError = Readonly<{
|
|
220
|
+
type: 'channel' | 'read' | 'apply' | 'subscriber' | 'not-started' | 'disposed' | 'runtime-in-use' | 'storage-subscription-unavailable';
|
|
221
|
+
}> | Readonly<{
|
|
222
|
+
type: 'validation';
|
|
223
|
+
issues: readonly ValidationIssue[];
|
|
224
|
+
}>;
|
|
225
|
+
|
|
226
|
+
export declare type ConsentSyncOptions = Readonly<{
|
|
227
|
+
consent: Pick<ConsentRuntime, 'getSnapshot' | 'restoreState' | 'subscribe'>;
|
|
228
|
+
/** Pass the complete wrapper; dropping subscribe loses post-cache-update invalidations. */
|
|
229
|
+
storage: Pick<ConsentStorage, 'load' | 'subscribe'>;
|
|
230
|
+
/** Scope to the exact application, user and consent record. Not an authentication boundary. */
|
|
231
|
+
channelName: string;
|
|
232
|
+
/** Defaults to a lazily constructed native BroadcastChannel at start(). */
|
|
233
|
+
createChannel?: (name: string) => ConsentSyncChannel;
|
|
234
|
+
/** Opt in for adapters needing post-cache-update invalidations. start() fails if subscribe is absent. */
|
|
235
|
+
requireStorageInvalidation?: boolean;
|
|
236
|
+
}>;
|
|
237
|
+
|
|
238
|
+
export declare type ConsentSyncResult = Readonly<{
|
|
239
|
+
success: true;
|
|
240
|
+
}> | Readonly<{
|
|
241
|
+
success: false;
|
|
242
|
+
error: ConsentSyncError;
|
|
243
|
+
}>;
|
|
244
|
+
|
|
245
|
+
export declare type ConsentSyncSnapshot = Readonly<{
|
|
246
|
+
state: 'idle' | 'active' | 'disposed';
|
|
247
|
+
}> | Readonly<{
|
|
248
|
+
state: 'error';
|
|
249
|
+
error: ConsentSyncError;
|
|
250
|
+
}>;
|
|
251
|
+
|
|
252
|
+
/** Synchronize a shared saved record, never policies, user commands or unpersisted choices. */
|
|
253
|
+
export declare function createConsentSync({ consent, storage, channelName, createChannel, requireStorageInvalidation, }: ConsentSyncOptions): ConsentSync;
|
|
254
|
+
|
|
255
|
+
declare type ExclusiveTarget<T> = T extends {
|
|
256
|
+
useId: string;
|
|
257
|
+
} ? T & {
|
|
258
|
+
readonly categoryId?: never;
|
|
259
|
+
} : T & {
|
|
260
|
+
readonly useId?: never;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** A validation issue without a copy of the rejected input. */
|
|
264
|
+
declare type ValidationIssue = Readonly<{
|
|
265
|
+
code: string;
|
|
266
|
+
path: readonly (string | number)[];
|
|
267
|
+
message: string;
|
|
268
|
+
}>;
|
|
269
|
+
|
|
270
|
+
declare type ValidationResult<T> = Readonly<{
|
|
271
|
+
success: true;
|
|
272
|
+
data: T;
|
|
273
|
+
}> | Readonly<{
|
|
274
|
+
success: false;
|
|
275
|
+
issues: readonly ValidationIssue[];
|
|
276
|
+
}>;
|
|
277
|
+
|
|
278
|
+
export { }
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { t as assertStorageIdentifier } from "../assertStorageIdentifier-BQuj-9bp.js";
|
|
2
|
+
//#region src/consent/areConsentStatesEqual.ts
|
|
3
|
+
/** Compare recorded choices, not permission or policy compatibility. Choice order is preserved. */
|
|
4
|
+
function areConsentStatesEqual(first, second) {
|
|
5
|
+
return first.choices.length === second.choices.length && first.choices.every((choice, index) => {
|
|
6
|
+
const other = second.choices[index];
|
|
7
|
+
return other?.useId === choice.useId && other.consentVersion === choice.consentVersion && other.decision === choice.decision;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/storage/sync/createConsentSyncStateTracker.ts
|
|
12
|
+
/** Own choice provenance only; the synchronizer owns I/O revisions and listener lifecycle. */
|
|
13
|
+
function createConsentSyncStateTracker(consent) {
|
|
14
|
+
let pending = false;
|
|
15
|
+
let restoring = false;
|
|
16
|
+
let observedState = consent.getSnapshot().state;
|
|
17
|
+
return {
|
|
18
|
+
observeLocalChange() {
|
|
19
|
+
const current = consent.getSnapshot().state;
|
|
20
|
+
if (!restoring && current !== observedState) pending = true;
|
|
21
|
+
observedState = current;
|
|
22
|
+
},
|
|
23
|
+
hasPendingChange: () => pending,
|
|
24
|
+
shouldApplyAfterRead: (before) => !pending && consent.getSnapshot().state === before,
|
|
25
|
+
acknowledge(savedState) {
|
|
26
|
+
if (savedState !== consent.getSnapshot().state) return false;
|
|
27
|
+
pending = false;
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
restore(state) {
|
|
31
|
+
restoring = true;
|
|
32
|
+
try {
|
|
33
|
+
const result = consent.restoreState(state);
|
|
34
|
+
const latest = consent.getSnapshot().state;
|
|
35
|
+
if (result.success && latest !== result.data.state) pending = true;
|
|
36
|
+
observedState = latest;
|
|
37
|
+
return result;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const latest = consent.getSnapshot().state;
|
|
40
|
+
if (!areConsentStatesEqual(latest, state)) pending = true;
|
|
41
|
+
observedState = latest;
|
|
42
|
+
throw error;
|
|
43
|
+
} finally {
|
|
44
|
+
restoring = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/consent/createConsentSubscription.ts
|
|
51
|
+
/** Synchronous invalidation notifications, not a log of every intermediate state. */
|
|
52
|
+
function createConsentSubscription() {
|
|
53
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
54
|
+
let notifying = false;
|
|
55
|
+
let pending = false;
|
|
56
|
+
return {
|
|
57
|
+
subscribe(listener) {
|
|
58
|
+
if (typeof listener !== "function") throw new TypeError("Expected a subscription listener.");
|
|
59
|
+
const subscription = () => listener();
|
|
60
|
+
listeners.add(subscription);
|
|
61
|
+
return () => {
|
|
62
|
+
listeners.delete(subscription);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
notify() {
|
|
66
|
+
pending = true;
|
|
67
|
+
if (notifying) return;
|
|
68
|
+
notifying = true;
|
|
69
|
+
const errors = [];
|
|
70
|
+
try {
|
|
71
|
+
do {
|
|
72
|
+
pending = false;
|
|
73
|
+
for (const listener of [...listeners]) {
|
|
74
|
+
if (!listeners.has(listener)) continue;
|
|
75
|
+
try {
|
|
76
|
+
listener();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
errors.push(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} while (pending);
|
|
82
|
+
} finally {
|
|
83
|
+
notifying = false;
|
|
84
|
+
}
|
|
85
|
+
if (errors.length > 0) throw new AggregateError(errors, "Consent subscribers failed after the change was committed.");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/storage/sync/createSyncStatus.ts
|
|
91
|
+
var success$1 = Object.freeze({ success: true });
|
|
92
|
+
/** Status observers must not turn a background refresh into an unhandled rejection. */
|
|
93
|
+
function createSyncStatus() {
|
|
94
|
+
let snapshot = Object.freeze({ state: "idle" });
|
|
95
|
+
let disposed = false;
|
|
96
|
+
const subscriptions = createConsentSubscription();
|
|
97
|
+
const unsubscribes = /* @__PURE__ */ new Set();
|
|
98
|
+
return {
|
|
99
|
+
getSnapshot: () => snapshot,
|
|
100
|
+
subscribe(listener) {
|
|
101
|
+
if (typeof listener !== "function") throw new TypeError("Expected a subscription listener.");
|
|
102
|
+
if (disposed) return () => {};
|
|
103
|
+
const remove = subscriptions.subscribe(listener);
|
|
104
|
+
const unsubscribe = () => {
|
|
105
|
+
remove();
|
|
106
|
+
unsubscribes.delete(unsubscribe);
|
|
107
|
+
};
|
|
108
|
+
unsubscribes.add(unsubscribe);
|
|
109
|
+
return unsubscribe;
|
|
110
|
+
},
|
|
111
|
+
set(next) {
|
|
112
|
+
if (disposed) return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: { type: "disposed" }
|
|
115
|
+
};
|
|
116
|
+
if (snapshot.state === next.state && next.state !== "error") return success$1;
|
|
117
|
+
snapshot = Object.freeze(next);
|
|
118
|
+
try {
|
|
119
|
+
subscriptions.notify();
|
|
120
|
+
} catch {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: next.state === "error" ? next.error : Object.freeze({ type: "subscriber" })
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return next.state === "error" ? {
|
|
127
|
+
success: false,
|
|
128
|
+
error: next.error
|
|
129
|
+
} : success$1;
|
|
130
|
+
},
|
|
131
|
+
dispose() {
|
|
132
|
+
disposed = true;
|
|
133
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/storage/sync/createConsentSync.ts
|
|
139
|
+
var success = Object.freeze({ success: true });
|
|
140
|
+
var notification = Object.freeze({
|
|
141
|
+
type: "peax-consent-changed",
|
|
142
|
+
formatVersion: 1
|
|
143
|
+
});
|
|
144
|
+
var owners = /* @__PURE__ */ new WeakMap();
|
|
145
|
+
function isNotification(value) {
|
|
146
|
+
return typeof value === "object" && value !== null && Object.keys(value).length === 2 && "type" in value && value.type === notification.type && "formatVersion" in value && value.formatVersion === 1;
|
|
147
|
+
}
|
|
148
|
+
/** Synchronize a shared saved record, never policies, user commands or unpersisted choices. */
|
|
149
|
+
function createConsentSync({ consent, storage, channelName, createChannel, requireStorageInvalidation }) {
|
|
150
|
+
assertStorageIdentifier(channelName, "channel name");
|
|
151
|
+
if (!consent || typeof consent.getSnapshot !== "function" || typeof consent.restoreState !== "function" || typeof consent.subscribe !== "function") throw new TypeError("Expected a consent runtime with getSnapshot, restoreState and subscribe.");
|
|
152
|
+
if (!storage || typeof storage.load !== "function") throw new TypeError("Expected consent storage with load.");
|
|
153
|
+
if (createChannel !== void 0 && typeof createChannel !== "function") throw new TypeError("Expected a channel factory.");
|
|
154
|
+
if (requireStorageInvalidation !== void 0 && typeof requireStorageInvalidation !== "boolean") throw new TypeError("Expected requireStorageInvalidation to be a boolean.");
|
|
155
|
+
const status = createSyncStatus();
|
|
156
|
+
const choices = createConsentSyncStateTracker(consent);
|
|
157
|
+
let revision = 0;
|
|
158
|
+
let disposed = false;
|
|
159
|
+
let wakeTarget;
|
|
160
|
+
let channel;
|
|
161
|
+
let startPromise;
|
|
162
|
+
let refreshPromise;
|
|
163
|
+
let unsubscribe;
|
|
164
|
+
let unsubscribeStorage;
|
|
165
|
+
function releaseOwnership() {
|
|
166
|
+
if (owners.get(consent) === status) owners.delete(consent);
|
|
167
|
+
}
|
|
168
|
+
function detachStorage() {
|
|
169
|
+
const detach = unsubscribeStorage;
|
|
170
|
+
unsubscribeStorage = void 0;
|
|
171
|
+
try {
|
|
172
|
+
detach?.();
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function fail(error) {
|
|
179
|
+
if (disposed) return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: { type: "disposed" }
|
|
182
|
+
};
|
|
183
|
+
return status.set({
|
|
184
|
+
state: "error",
|
|
185
|
+
error: Object.freeze(error)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function failClosed(error) {
|
|
189
|
+
choices.restore({ choices: [] });
|
|
190
|
+
return fail(error);
|
|
191
|
+
}
|
|
192
|
+
async function read(request) {
|
|
193
|
+
if (disposed) return {
|
|
194
|
+
success: false,
|
|
195
|
+
error: { type: "disposed" }
|
|
196
|
+
};
|
|
197
|
+
if (!channel) return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: { type: "not-started" }
|
|
200
|
+
};
|
|
201
|
+
if (choices.hasPendingChange()) return status.getSnapshot().state === "idle" ? status.set({ state: "active" }) : success;
|
|
202
|
+
let before;
|
|
203
|
+
try {
|
|
204
|
+
before = consent.getSnapshot().state;
|
|
205
|
+
} catch {
|
|
206
|
+
return fail({ type: "apply" });
|
|
207
|
+
}
|
|
208
|
+
let loaded;
|
|
209
|
+
try {
|
|
210
|
+
loaded = await storage.load();
|
|
211
|
+
} catch {
|
|
212
|
+
loaded = {
|
|
213
|
+
success: false,
|
|
214
|
+
error: { type: "read" }
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (disposed) return {
|
|
218
|
+
success: false,
|
|
219
|
+
error: { type: "disposed" }
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
if (request !== revision || !choices.shouldApplyAfterRead(before)) return success;
|
|
223
|
+
if (!loaded.success) return failClosed(loaded.error.type === "validation" ? loaded.error : { type: "read" });
|
|
224
|
+
const restored = choices.restore(loaded.data ?? { choices: [] });
|
|
225
|
+
if (disposed) return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: { type: "disposed" }
|
|
228
|
+
};
|
|
229
|
+
if (!restored.success) return failClosed({
|
|
230
|
+
type: "validation",
|
|
231
|
+
issues: restored.issues
|
|
232
|
+
});
|
|
233
|
+
return status.set({ state: "active" });
|
|
234
|
+
} catch {
|
|
235
|
+
return fail({ type: "apply" });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function refresh() {
|
|
239
|
+
if (disposed || !channel || choices.hasPendingChange()) return read(revision);
|
|
240
|
+
revision += 1;
|
|
241
|
+
refreshPromise ??= Promise.resolve().then(async () => {
|
|
242
|
+
try {
|
|
243
|
+
let request;
|
|
244
|
+
let result;
|
|
245
|
+
do {
|
|
246
|
+
request = revision;
|
|
247
|
+
result = await read(request);
|
|
248
|
+
} while (!disposed && !choices.hasPendingChange() && request !== revision);
|
|
249
|
+
return result;
|
|
250
|
+
} finally {
|
|
251
|
+
refreshPromise = void 0;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return refreshPromise;
|
|
255
|
+
}
|
|
256
|
+
const received = (event) => {
|
|
257
|
+
if (isNotification(event.data)) refresh();
|
|
258
|
+
};
|
|
259
|
+
const wake = () => {
|
|
260
|
+
refresh();
|
|
261
|
+
};
|
|
262
|
+
return Object.freeze({
|
|
263
|
+
getSnapshot: status.getSnapshot,
|
|
264
|
+
subscribe: status.subscribe,
|
|
265
|
+
refresh,
|
|
266
|
+
start() {
|
|
267
|
+
if (disposed) return Promise.resolve({
|
|
268
|
+
success: false,
|
|
269
|
+
error: { type: "disposed" }
|
|
270
|
+
});
|
|
271
|
+
if (startPromise) return startPromise;
|
|
272
|
+
if (owners.has(consent)) return Promise.resolve(fail({ type: "runtime-in-use" }));
|
|
273
|
+
if (requireStorageInvalidation && typeof storage.subscribe !== "function") return Promise.resolve(fail({ type: "storage-subscription-unavailable" }));
|
|
274
|
+
owners.set(consent, status);
|
|
275
|
+
try {
|
|
276
|
+
channel = createChannel ? createChannel(channelName) : new BroadcastChannel(channelName);
|
|
277
|
+
if (disposed) {
|
|
278
|
+
channel.close();
|
|
279
|
+
channel = void 0;
|
|
280
|
+
return Promise.resolve({
|
|
281
|
+
success: false,
|
|
282
|
+
error: { type: "disposed" }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
channel.addEventListener("message", received);
|
|
286
|
+
choices.observeLocalChange();
|
|
287
|
+
unsubscribe = consent.subscribe(choices.observeLocalChange);
|
|
288
|
+
unsubscribeStorage = storage.subscribe?.(wake);
|
|
289
|
+
if (disposed) {
|
|
290
|
+
detachStorage();
|
|
291
|
+
return Promise.resolve({
|
|
292
|
+
success: false,
|
|
293
|
+
error: { type: "disposed" }
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
wakeTarget = typeof window === "undefined" ? void 0 : window;
|
|
297
|
+
wakeTarget?.addEventListener("focus", wake);
|
|
298
|
+
wakeTarget?.addEventListener("pageshow", wake);
|
|
299
|
+
startPromise = refresh();
|
|
300
|
+
return startPromise;
|
|
301
|
+
} catch {
|
|
302
|
+
try {
|
|
303
|
+
channel?.removeEventListener("message", received);
|
|
304
|
+
} catch {}
|
|
305
|
+
try {
|
|
306
|
+
channel?.close();
|
|
307
|
+
} catch {}
|
|
308
|
+
channel = void 0;
|
|
309
|
+
releaseOwnership();
|
|
310
|
+
unsubscribe?.();
|
|
311
|
+
unsubscribe = void 0;
|
|
312
|
+
detachStorage();
|
|
313
|
+
wakeTarget?.removeEventListener("focus", wake);
|
|
314
|
+
wakeTarget?.removeEventListener("pageshow", wake);
|
|
315
|
+
return Promise.resolve(fail({ type: "channel" }));
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
notify(savedState) {
|
|
319
|
+
if (disposed) return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: { type: "disposed" }
|
|
322
|
+
};
|
|
323
|
+
if (!channel) return {
|
|
324
|
+
success: false,
|
|
325
|
+
error: { type: "not-started" }
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
channel.postMessage(notification);
|
|
329
|
+
if (disposed) return {
|
|
330
|
+
success: false,
|
|
331
|
+
error: { type: "disposed" }
|
|
332
|
+
};
|
|
333
|
+
if (choices.acknowledge(savedState)) refresh();
|
|
334
|
+
return success;
|
|
335
|
+
} catch {
|
|
336
|
+
return fail({ type: "channel" });
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
dispose() {
|
|
340
|
+
if (disposed) return success;
|
|
341
|
+
disposed = true;
|
|
342
|
+
revision += 1;
|
|
343
|
+
releaseOwnership();
|
|
344
|
+
unsubscribe?.();
|
|
345
|
+
unsubscribe = void 0;
|
|
346
|
+
wakeTarget?.removeEventListener("focus", wake);
|
|
347
|
+
wakeTarget?.removeEventListener("pageshow", wake);
|
|
348
|
+
let failed = !detachStorage();
|
|
349
|
+
try {
|
|
350
|
+
channel?.removeEventListener("message", received);
|
|
351
|
+
} catch {
|
|
352
|
+
failed = true;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
channel?.close();
|
|
356
|
+
} catch {
|
|
357
|
+
failed = true;
|
|
358
|
+
}
|
|
359
|
+
channel = void 0;
|
|
360
|
+
const result = status.set({ state: "disposed" });
|
|
361
|
+
status.dispose();
|
|
362
|
+
return failed ? {
|
|
363
|
+
success: false,
|
|
364
|
+
error: { type: "channel" }
|
|
365
|
+
} : result;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
export { createConsentSync };
|