@wix/web5-core 1.63.15 → 1.63.17

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.
Files changed (82) hide show
  1. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js +36 -32
  2. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  3. package/dist/cjs/component/componentDefinitions/index.js +5 -4
  4. package/dist/cjs/component/componentDefinitions/index.js.map +1 -1
  5. package/dist/cjs/component/componentDefinitions/parse-utils.js +43 -6
  6. package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
  7. package/dist/cjs/index.js +32 -3
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/match/astToParts.js +14 -0
  10. package/dist/cjs/match/astToParts.js.map +1 -1
  11. package/dist/cjs/privacy/consentGate.js +275 -0
  12. package/dist/cjs/privacy/consentGate.js.map +1 -0
  13. package/dist/cjs/privacy/consentOverride.js +94 -0
  14. package/dist/cjs/privacy/consentOverride.js.map +1 -0
  15. package/dist/cjs/privacy/detectProvider.js +71 -0
  16. package/dist/cjs/privacy/detectProvider.js.map +1 -0
  17. package/dist/cjs/privacy/index.js +40 -0
  18. package/dist/cjs/privacy/index.js.map +1 -0
  19. package/dist/cjs/privacy/providers/hostSuppliedProvider.js +89 -0
  20. package/dist/cjs/privacy/providers/hostSuppliedProvider.js.map +1 -0
  21. package/dist/cjs/privacy/providers/oneTrustProvider.js +72 -0
  22. package/dist/cjs/privacy/providers/oneTrustProvider.js.map +1 -0
  23. package/dist/cjs/privacy/providers/shopifyProvider.js +177 -0
  24. package/dist/cjs/privacy/providers/shopifyProvider.js.map +1 -0
  25. package/dist/cjs/privacy/types.js +34 -0
  26. package/dist/cjs/privacy/types.js.map +1 -0
  27. package/dist/cjs/utils/analyticsEvents.js +30 -25
  28. package/dist/cjs/utils/analyticsEvents.js.map +1 -1
  29. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js +37 -33
  30. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  31. package/dist/esm/component/componentDefinitions/index.js +5 -4
  32. package/dist/esm/component/componentDefinitions/index.js.map +1 -1
  33. package/dist/esm/component/componentDefinitions/parse-utils.js +42 -6
  34. package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
  35. package/dist/esm/index.js +3 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/match/astToParts.js +14 -0
  38. package/dist/esm/match/astToParts.js.map +1 -1
  39. package/dist/esm/privacy/consentGate.js +265 -0
  40. package/dist/esm/privacy/consentGate.js.map +1 -0
  41. package/dist/esm/privacy/consentOverride.js +87 -0
  42. package/dist/esm/privacy/consentOverride.js.map +1 -0
  43. package/dist/esm/privacy/detectProvider.js +65 -0
  44. package/dist/esm/privacy/detectProvider.js.map +1 -0
  45. package/dist/esm/privacy/index.js +8 -0
  46. package/dist/esm/privacy/index.js.map +1 -0
  47. package/dist/esm/privacy/providers/hostSuppliedProvider.js +81 -0
  48. package/dist/esm/privacy/providers/hostSuppliedProvider.js.map +1 -0
  49. package/dist/esm/privacy/providers/oneTrustProvider.js +66 -0
  50. package/dist/esm/privacy/providers/oneTrustProvider.js.map +1 -0
  51. package/dist/esm/privacy/providers/shopifyProvider.js +174 -0
  52. package/dist/esm/privacy/providers/shopifyProvider.js.map +1 -0
  53. package/dist/esm/privacy/types.js +30 -0
  54. package/dist/esm/privacy/types.js.map +1 -0
  55. package/dist/esm/utils/analyticsEvents.js +30 -25
  56. package/dist/esm/utils/analyticsEvents.js.map +1 -1
  57. package/dist/types/component/componentDefinitions/TextBlockSectionDefinition.d.ts.map +1 -1
  58. package/dist/types/component/componentDefinitions/index.d.ts.map +1 -1
  59. package/dist/types/component/componentDefinitions/parse-utils.d.ts +21 -0
  60. package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
  61. package/dist/types/index.d.ts +1 -0
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/match/astToParts.d.ts.map +1 -1
  64. package/dist/types/privacy/consentGate.d.ts +85 -0
  65. package/dist/types/privacy/consentGate.d.ts.map +1 -0
  66. package/dist/types/privacy/consentOverride.d.ts +33 -0
  67. package/dist/types/privacy/consentOverride.d.ts.map +1 -0
  68. package/dist/types/privacy/detectProvider.d.ts +25 -0
  69. package/dist/types/privacy/detectProvider.d.ts.map +1 -0
  70. package/dist/types/privacy/index.d.ts +8 -0
  71. package/dist/types/privacy/index.d.ts.map +1 -0
  72. package/dist/types/privacy/providers/hostSuppliedProvider.d.ts +29 -0
  73. package/dist/types/privacy/providers/hostSuppliedProvider.d.ts.map +1 -0
  74. package/dist/types/privacy/providers/oneTrustProvider.d.ts +15 -0
  75. package/dist/types/privacy/providers/oneTrustProvider.d.ts.map +1 -0
  76. package/dist/types/privacy/providers/shopifyProvider.d.ts +26 -0
  77. package/dist/types/privacy/providers/shopifyProvider.d.ts.map +1 -0
  78. package/dist/types/privacy/types.d.ts +32 -0
  79. package/dist/types/privacy/types.d.ts.map +1 -0
  80. package/dist/types/utils/analyticsEvents.d.ts +9 -0
  81. package/dist/types/utils/analyticsEvents.d.ts.map +1 -1
  82. package/package.json +2 -2
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.unlockOnUserAction = exports.transmit = exports.subscribeToConsent = exports.setConsentBufferLimit = exports.resetConsentGateForTests = exports.mayTransmit = exports.mayPersistIdentity = exports.isUserEngaged = exports.installConsentProvider = exports.getInstalledProviderName = exports.getGateView = exports.getConsentSnapshot = exports.getConsentGateStats = void 0;
5
+ var _types = require("./types");
6
+ /**
7
+ * The consent gate — DL #218.
8
+ *
9
+ * Every BI, telemetry and analytics emitter in web5 goes through `transmit()`
10
+ * instead of calling its transport directly. The gate exists because the
11
+ * consent signal resolves *after* the events that need it: Shopify's consent
12
+ * API is not on the page by default and has to be requested, and the fedops
13
+ * app-load pair fires inside that window. A boolean checked at each call site
14
+ * can only ever drop an event that is already in flight; the gate can hold it.
15
+ *
16
+ * Two independent unlocks, and they do not compose as a plain "either/or":
17
+ *
18
+ * | consent | engaged | result |
19
+ * |-----------|---------|---------------------------------|
20
+ * | granted | either | transmit |
21
+ * | unknown | no | hold in the buffer |
22
+ * | unknown | yes | transmit, and flush what's held |
23
+ * | denied | yes | still nothing |
24
+ *
25
+ * Denied outranks engaged. A visitor who refused analytics in the banner does
26
+ * not re-enable tracking by typing a question.
27
+ */
28
+
29
+ /** Held events are dropped past this many — a runaway emitter must not become a leak. */
30
+ const DEFAULT_BUFFER_LIMIT = 50;
31
+
32
+ /**
33
+ * One prefix for the whole consent trail, so `[w5-consent]` in the console
34
+ * filter shows the full story: which CMP was found, what it answered, and for
35
+ * every event whether it was sent, held, dropped or replayed.
36
+ */
37
+ const LOG_PREFIX = '[w5-consent]';
38
+ const log = (...args) => {
39
+ try {
40
+ console.log(LOG_PREFIX, ...args);
41
+ } catch {
42
+ // A console that throws must never break a send.
43
+ }
44
+ };
45
+ const describe = s => `analytics=${s.analytics} performance=${s.performance} marketing=${s.marketing}`;
46
+ let snapshot = _types.UNKNOWN_CONSENT;
47
+ let engaged = false;
48
+ let provider = null;
49
+ let unsubscribeProvider = null;
50
+ let buffer = [];
51
+ let bufferLimit = DEFAULT_BUFFER_LIMIT;
52
+ let droppedFromOverflow = 0;
53
+ const listeners = new Set();
54
+
55
+ /**
56
+ * Cached so `getGateView` is referentially stable between changes —
57
+ * `useSyncExternalStore` re-renders forever if the snapshot is a fresh object
58
+ * on every read.
59
+ */
60
+ let view = Object.freeze({
61
+ consent: _types.UNKNOWN_CONSENT,
62
+ engaged: false
63
+ });
64
+ const rebuildView = () => {
65
+ view = Object.freeze({
66
+ consent: snapshot,
67
+ engaged
68
+ });
69
+ };
70
+ const notify = () => {
71
+ for (const listener of listeners) {
72
+ try {
73
+ listener();
74
+ } catch {
75
+ // A subscriber that throws must not stop the others, or stop a flush.
76
+ }
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Whether an event for `purpose` may go on the wire right now.
82
+ *
83
+ * `necessary` is always allowed. For everything else: explicit consent wins in
84
+ * both directions, and engagement resolves only the `unknown` case.
85
+ */
86
+ const mayTransmit = purpose => {
87
+ if (purpose === 'necessary') {
88
+ return true;
89
+ }
90
+ const state = snapshot[purpose];
91
+ if (state === 'denied') {
92
+ return false;
93
+ }
94
+ if (state === 'granted') {
95
+ return true;
96
+ }
97
+ return engaged;
98
+ };
99
+
100
+ /**
101
+ * Release everything the gate is holding that is now allowed, and drop
102
+ * everything that is now refused. Anything still `unknown` stays held.
103
+ *
104
+ * Order is preserved among released events, but a flush necessarily emits them
105
+ * later than they were raised — a held app-load event reaches the wire after
106
+ * the prompt that unlocked it. Anything reading these as a timeline has to
107
+ * know that.
108
+ */
109
+ exports.mayTransmit = mayTransmit;
110
+ const settleBuffer = () => {
111
+ if (buffer.length === 0) {
112
+ return;
113
+ }
114
+ const stillHeld = [];
115
+ const releasing = [];
116
+ for (const held of buffer) {
117
+ if (mayTransmit(held.purpose)) {
118
+ releasing.push(held);
119
+ } else if (snapshot[held.purpose] === 'denied') {
120
+ // dropped on the floor, deliberately and permanently
121
+ } else {
122
+ stillHeld.push(held);
123
+ }
124
+ }
125
+ const dropped = buffer.length - stillHeld.length - releasing.length;
126
+ buffer = stillHeld;
127
+ if (releasing.length || dropped) {
128
+ log(`settle — releasing ${releasing.length}` + (dropped ? `, dropping ${dropped} (refused)` : '') + (stillHeld.length ? `, still holding ${stillHeld.length}` : ''));
129
+ }
130
+ for (const held of releasing) {
131
+ try {
132
+ log(` ↳ REPLAY ${held.label} (${held.purpose})`);
133
+ held.send();
134
+ } catch {
135
+ // A failed send must not strand the rest of the flush.
136
+ }
137
+ }
138
+ };
139
+
140
+ /**
141
+ * The single entry point for anything that would put a request on the wire.
142
+ *
143
+ * Allowed → sent now. Refused → dropped. Not yet known → held until an unlock
144
+ * settles it, or discarded with the page.
145
+ */
146
+ const transmit = (purpose, send, label = 'event') => {
147
+ if (mayTransmit(purpose)) {
148
+ log(`SEND ${label} (${purpose}) — allowed`);
149
+ send();
150
+ return;
151
+ }
152
+ if (purpose !== 'necessary' && snapshot[purpose] === 'denied') {
153
+ log(`DROP ${label} (${purpose}) — visitor refused`);
154
+ return;
155
+ }
156
+ if (buffer.length >= bufferLimit) {
157
+ droppedFromOverflow += 1;
158
+ log(`DROP ${label} (${purpose}) — buffer full at ${bufferLimit}, ${droppedFromOverflow} lost`);
159
+ return;
160
+ }
161
+ buffer.push({
162
+ purpose: purpose,
163
+ send,
164
+ label
165
+ });
166
+ log(`HOLD ${label} (${purpose}) — no answer yet, ${buffer.length} held`);
167
+ };
168
+
169
+ /**
170
+ * The visitor did something deliberate — submitted a prompt — so the session
171
+ * may be measured even though no CMP has answered.
172
+ *
173
+ * This is the unlock that keeps web5 measurable at all: on a Shopify store
174
+ * with no privacy configuration the consent API never loads, consent stays
175
+ * `unknown` forever, and without this every store would report nothing.
176
+ *
177
+ * It unlocks **transmission only**. Persistent identity stays gated on real
178
+ * consent (see `mayPersistIdentity`) — typing a question is a reason to
179
+ * measure the session, not a reason to persist an identifier across visits.
180
+ * It also cannot override a `denied`.
181
+ */
182
+ exports.transmit = transmit;
183
+ const unlockOnUserAction = () => {
184
+ if (engaged) {
185
+ return;
186
+ }
187
+ engaged = true;
188
+ log(`UNLOCK — visitor submitted a prompt; ${buffer.length} held event(s) to settle`);
189
+ rebuildView();
190
+ settleBuffer();
191
+ notify();
192
+ };
193
+
194
+ /** Whether the visitor has taken the deliberate action that unlocks the session. */
195
+ exports.unlockOnUserAction = unlockOnUserAction;
196
+ const isUserEngaged = () => engaged;
197
+
198
+ /**
199
+ * Persistent, cross-visit storage of a visitor identifier requires real
200
+ * consent — engagement is deliberately not enough.
201
+ */
202
+ exports.isUserEngaged = isUserEngaged;
203
+ const mayPersistIdentity = () => snapshot.analytics === 'granted';
204
+ exports.mayPersistIdentity = mayPersistIdentity;
205
+ const getConsentSnapshot = () => snapshot;
206
+
207
+ /** Referentially stable between changes, for `useSyncExternalStore`. */
208
+ exports.getConsentSnapshot = getConsentSnapshot;
209
+ const getGateView = () => view;
210
+ exports.getGateView = getGateView;
211
+ const subscribeToConsent = listener => {
212
+ listeners.add(listener);
213
+ return () => {
214
+ listeners.delete(listener);
215
+ };
216
+ };
217
+ exports.subscribeToConsent = subscribeToConsent;
218
+ const applySnapshot = next => {
219
+ if (next.analytics === snapshot.analytics && next.marketing === snapshot.marketing && next.performance === snapshot.performance) {
220
+ return;
221
+ }
222
+ log(`ANSWER — ${describe(snapshot)} → ${describe(next)}`);
223
+ snapshot = Object.freeze({
224
+ ...next
225
+ });
226
+ rebuildView();
227
+ settleBuffer();
228
+ notify();
229
+ };
230
+
231
+ /**
232
+ * Install the host's provider. Reads its current state immediately, *then*
233
+ * subscribes — DL #217: a listener registered into an already-rendered page is
234
+ * bound to an event that may have fired long before the bundle executed, so
235
+ * subscribing alone would leave the gate permanently at `unknown`.
236
+ */
237
+ const installConsentProvider = next => {
238
+ unsubscribeProvider == null || unsubscribeProvider();
239
+ provider = next;
240
+ const initial = next.read();
241
+ log(`provider installed: ${next.name} — reads ${describe(initial)}`);
242
+ applySnapshot(initial);
243
+ unsubscribeProvider = next.subscribe(applySnapshot);
244
+ };
245
+ exports.installConsentProvider = installConsentProvider;
246
+ const getInstalledProviderName = () => {
247
+ var _provider;
248
+ return ((_provider = provider) == null ? void 0 : _provider.name) ?? null;
249
+ };
250
+ exports.getInstalledProviderName = getInstalledProviderName;
251
+ const setConsentBufferLimit = limit => {
252
+ bufferLimit = Math.max(0, limit);
253
+ };
254
+
255
+ /** Diagnostics only — how much the gate is holding, and what it had to drop. */
256
+ exports.setConsentBufferLimit = setConsentBufferLimit;
257
+ const getConsentGateStats = () => ({
258
+ held: buffer.length,
259
+ droppedFromOverflow
260
+ });
261
+ exports.getConsentGateStats = getConsentGateStats;
262
+ const resetConsentGateForTests = () => {
263
+ unsubscribeProvider == null || unsubscribeProvider();
264
+ unsubscribeProvider = null;
265
+ provider = null;
266
+ snapshot = _types.UNKNOWN_CONSENT;
267
+ engaged = false;
268
+ buffer = [];
269
+ bufferLimit = DEFAULT_BUFFER_LIMIT;
270
+ droppedFromOverflow = 0;
271
+ listeners.clear();
272
+ rebuildView();
273
+ };
274
+ exports.resetConsentGateForTests = resetConsentGateForTests;
275
+ //# sourceMappingURL=consentGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_types","require","DEFAULT_BUFFER_LIMIT","LOG_PREFIX","log","args","console","describe","s","analytics","performance","marketing","snapshot","UNKNOWN_CONSENT","engaged","provider","unsubscribeProvider","buffer","bufferLimit","droppedFromOverflow","listeners","Set","view","Object","freeze","consent","rebuildView","notify","listener","mayTransmit","purpose","state","exports","settleBuffer","length","stillHeld","releasing","held","push","dropped","label","send","transmit","unlockOnUserAction","isUserEngaged","mayPersistIdentity","getConsentSnapshot","getGateView","subscribeToConsent","add","delete","applySnapshot","next","installConsentProvider","initial","read","name","subscribe","getInstalledProviderName","_provider","setConsentBufferLimit","limit","Math","max","getConsentGateStats","resetConsentGateForTests","clear"],"sources":["../../../src/privacy/consentGate.ts"],"sourcesContent":["/**\n * The consent gate — DL #218.\n *\n * Every BI, telemetry and analytics emitter in web5 goes through `transmit()`\n * instead of calling its transport directly. The gate exists because the\n * consent signal resolves *after* the events that need it: Shopify's consent\n * API is not on the page by default and has to be requested, and the fedops\n * app-load pair fires inside that window. A boolean checked at each call site\n * can only ever drop an event that is already in flight; the gate can hold it.\n *\n * Two independent unlocks, and they do not compose as a plain \"either/or\":\n *\n * | consent | engaged | result |\n * |-----------|---------|---------------------------------|\n * | granted | either | transmit |\n * | unknown | no | hold in the buffer |\n * | unknown | yes | transmit, and flush what's held |\n * | denied | yes | still nothing |\n *\n * Denied outranks engaged. A visitor who refused analytics in the banner does\n * not re-enable tracking by typing a question.\n */\n\nimport {\n UNKNOWN_CONSENT,\n type ConsentProvider,\n type ConsentPurpose,\n type ConsentSnapshot,\n type GatedPurpose,\n} from './types';\n\n/** Held events are dropped past this many — a runaway emitter must not become a leak. */\nconst DEFAULT_BUFFER_LIMIT = 50;\n\n/**\n * One prefix for the whole consent trail, so `[w5-consent]` in the console\n * filter shows the full story: which CMP was found, what it answered, and for\n * every event whether it was sent, held, dropped or replayed.\n */\nconst LOG_PREFIX = '[w5-consent]';\n\nconst log = (...args: unknown[]): void => {\n try {\n console.log(LOG_PREFIX, ...args);\n } catch {\n // A console that throws must never break a send.\n }\n};\n\nconst describe = (s: ConsentSnapshot): string =>\n `analytics=${s.analytics} performance=${s.performance} marketing=${s.marketing}`;\n\ninterface HeldEvent {\n purpose: GatedPurpose;\n send: () => void;\n label: string;\n}\n\nexport interface GateView {\n consent: ConsentSnapshot;\n engaged: boolean;\n}\n\ntype Listener = () => void;\n\nlet snapshot: ConsentSnapshot = UNKNOWN_CONSENT;\nlet engaged = false;\nlet provider: ConsentProvider | null = null;\nlet unsubscribeProvider: (() => void) | null = null;\nlet buffer: HeldEvent[] = [];\nlet bufferLimit = DEFAULT_BUFFER_LIMIT;\nlet droppedFromOverflow = 0;\n\nconst listeners = new Set<Listener>();\n\n/**\n * Cached so `getGateView` is referentially stable between changes —\n * `useSyncExternalStore` re-renders forever if the snapshot is a fresh object\n * on every read.\n */\nlet view: GateView = Object.freeze({ consent: UNKNOWN_CONSENT, engaged: false });\n\nconst rebuildView = (): void => {\n view = Object.freeze({ consent: snapshot, engaged });\n};\n\nconst notify = (): void => {\n for (const listener of listeners) {\n try {\n listener();\n } catch {\n // A subscriber that throws must not stop the others, or stop a flush.\n }\n }\n};\n\n/**\n * Whether an event for `purpose` may go on the wire right now.\n *\n * `necessary` is always allowed. For everything else: explicit consent wins in\n * both directions, and engagement resolves only the `unknown` case.\n */\nexport const mayTransmit = (purpose: ConsentPurpose): boolean => {\n if (purpose === 'necessary') {\n return true;\n }\n const state = snapshot[purpose];\n if (state === 'denied') {\n return false;\n }\n if (state === 'granted') {\n return true;\n }\n return engaged;\n};\n\n/**\n * Release everything the gate is holding that is now allowed, and drop\n * everything that is now refused. Anything still `unknown` stays held.\n *\n * Order is preserved among released events, but a flush necessarily emits them\n * later than they were raised — a held app-load event reaches the wire after\n * the prompt that unlocked it. Anything reading these as a timeline has to\n * know that.\n */\nconst settleBuffer = (): void => {\n if (buffer.length === 0) {\n return;\n }\n const stillHeld: HeldEvent[] = [];\n const releasing: HeldEvent[] = [];\n\n for (const held of buffer) {\n if (mayTransmit(held.purpose)) {\n releasing.push(held);\n } else if (snapshot[held.purpose] === 'denied') {\n // dropped on the floor, deliberately and permanently\n } else {\n stillHeld.push(held);\n }\n }\n\n const dropped = buffer.length - stillHeld.length - releasing.length;\n buffer = stillHeld;\n\n if (releasing.length || dropped) {\n log(\n `settle — releasing ${releasing.length}` +\n (dropped ? `, dropping ${dropped} (refused)` : '') +\n (stillHeld.length ? `, still holding ${stillHeld.length}` : ''),\n );\n }\n\n for (const held of releasing) {\n try {\n log(` ↳ REPLAY ${held.label} (${held.purpose})`);\n held.send();\n } catch {\n // A failed send must not strand the rest of the flush.\n }\n }\n};\n\n/**\n * The single entry point for anything that would put a request on the wire.\n *\n * Allowed → sent now. Refused → dropped. Not yet known → held until an unlock\n * settles it, or discarded with the page.\n */\nexport const transmit = (\n purpose: ConsentPurpose,\n send: () => void,\n label = 'event',\n): void => {\n if (mayTransmit(purpose)) {\n log(`SEND ${label} (${purpose}) — allowed`);\n send();\n return;\n }\n if (purpose !== 'necessary' && snapshot[purpose] === 'denied') {\n log(`DROP ${label} (${purpose}) — visitor refused`);\n return;\n }\n if (buffer.length >= bufferLimit) {\n droppedFromOverflow += 1;\n log(\n `DROP ${label} (${purpose}) — buffer full at ${bufferLimit}, ${droppedFromOverflow} lost`,\n );\n return;\n }\n buffer.push({ purpose: purpose as GatedPurpose, send, label });\n log(`HOLD ${label} (${purpose}) — no answer yet, ${buffer.length} held`);\n};\n\n/**\n * The visitor did something deliberate — submitted a prompt — so the session\n * may be measured even though no CMP has answered.\n *\n * This is the unlock that keeps web5 measurable at all: on a Shopify store\n * with no privacy configuration the consent API never loads, consent stays\n * `unknown` forever, and without this every store would report nothing.\n *\n * It unlocks **transmission only**. Persistent identity stays gated on real\n * consent (see `mayPersistIdentity`) — typing a question is a reason to\n * measure the session, not a reason to persist an identifier across visits.\n * It also cannot override a `denied`.\n */\nexport const unlockOnUserAction = (): void => {\n if (engaged) {\n return;\n }\n engaged = true;\n log(\n `UNLOCK — visitor submitted a prompt; ${buffer.length} held event(s) to settle`,\n );\n rebuildView();\n settleBuffer();\n notify();\n};\n\n/** Whether the visitor has taken the deliberate action that unlocks the session. */\nexport const isUserEngaged = (): boolean => engaged;\n\n/**\n * Persistent, cross-visit storage of a visitor identifier requires real\n * consent — engagement is deliberately not enough.\n */\nexport const mayPersistIdentity = (): boolean => snapshot.analytics === 'granted';\n\nexport const getConsentSnapshot = (): ConsentSnapshot => snapshot;\n\n/** Referentially stable between changes, for `useSyncExternalStore`. */\nexport const getGateView = (): GateView => view;\n\nexport const subscribeToConsent = (listener: Listener): (() => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\nconst applySnapshot = (next: ConsentSnapshot): void => {\n if (\n next.analytics === snapshot.analytics &&\n next.marketing === snapshot.marketing &&\n next.performance === snapshot.performance\n ) {\n return;\n }\n log(`ANSWER — ${describe(snapshot)} → ${describe(next)}`);\n snapshot = Object.freeze({ ...next });\n rebuildView();\n settleBuffer();\n notify();\n};\n\n/**\n * Install the host's provider. Reads its current state immediately, *then*\n * subscribes — DL #217: a listener registered into an already-rendered page is\n * bound to an event that may have fired long before the bundle executed, so\n * subscribing alone would leave the gate permanently at `unknown`.\n */\nexport const installConsentProvider = (next: ConsentProvider): void => {\n unsubscribeProvider?.();\n provider = next;\n const initial = next.read();\n log(`provider installed: ${next.name} — reads ${describe(initial)}`);\n applySnapshot(initial);\n unsubscribeProvider = next.subscribe(applySnapshot);\n};\n\nexport const getInstalledProviderName = (): string | null => provider?.name ?? null;\n\nexport const setConsentBufferLimit = (limit: number): void => {\n bufferLimit = Math.max(0, limit);\n};\n\n/** Diagnostics only — how much the gate is holding, and what it had to drop. */\nexport const getConsentGateStats = (): {\n held: number;\n droppedFromOverflow: number;\n} => ({ held: buffer.length, droppedFromOverflow });\n\nexport const resetConsentGateForTests = (): void => {\n unsubscribeProvider?.();\n unsubscribeProvider = null;\n provider = null;\n snapshot = UNKNOWN_CONSENT;\n engaged = false;\n buffer = [];\n bufferLimit = DEFAULT_BUFFER_LIMIT;\n droppedFromOverflow = 0;\n listeners.clear();\n rebuildView();\n};\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.setConsentOverride = exports.overrideSnapshot = exports.getConsentOverride = exports.CONSENT_OVERRIDE_QUERY_PARAM = exports.CONSENT_OVERRIDE_KEY = void 0;
5
+ /**
6
+ * Consent override (debug-only) — DL #218.
7
+ *
8
+ * Shopify only serves its cookie banner where consent is legally required, so
9
+ * from most of the world `shouldShowBanner()` is `false` and there is no way to
10
+ * reach the `unknown` or `denied` branches by hand. Development stores often
11
+ * have no privacy configuration at all, which pins the gate at `unknown`
12
+ * forever. Neither is a bug, and both make the interesting states unreachable
13
+ * from a browser.
14
+ *
15
+ * This forces the gate's answer so all four rows of the truth table can be
16
+ * walked on any store, in any region, published or not.
17
+ *
18
+ * ?w5consent=granted | denied | unknown (one-shot)
19
+ * localStorage["w5_consent_override"] (sticky)
20
+ *
21
+ * **The two are not equally trusted, on purpose.** A link is something one
22
+ * person can send another, so a query param may only ever *reduce* what is
23
+ * collected: `denied` is honoured anywhere, while `granted` and `unknown` are
24
+ * honoured only on a local origin. Turning tracking *on* for someone else by
25
+ * sending them a URL is exactly the thing this whole feature exists to
26
+ * prevent. On a real storefront, set the localStorage key in devtools — which
27
+ * is a deliberate act by the person whose browser it is.
28
+ */
29
+
30
+ const CONSENT_OVERRIDE_KEY = exports.CONSENT_OVERRIDE_KEY = 'w5_consent_override';
31
+ const CONSENT_OVERRIDE_QUERY_PARAM = exports.CONSENT_OVERRIDE_QUERY_PARAM = 'w5consent';
32
+ const VALID = ['granted', 'denied', 'unknown'];
33
+ const isConsentState = value => typeof value === 'string' && VALID.includes(value);
34
+
35
+ /** Only a local origin may force a *more* permissive state from a URL. */
36
+ const isLocalOrigin = () => {
37
+ try {
38
+ const {
39
+ hostname
40
+ } = window.location;
41
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local');
42
+ } catch {
43
+ return false;
44
+ }
45
+ };
46
+ const readQueryParam = () => {
47
+ try {
48
+ const raw = new URLSearchParams(window.location.search).get(CONSENT_OVERRIDE_QUERY_PARAM);
49
+ if (!isConsentState(raw)) {
50
+ return null;
51
+ }
52
+ // `denied` can only ever collect less, so a link may set it anywhere.
53
+ if (raw === 'denied' || isLocalOrigin()) {
54
+ return raw;
55
+ }
56
+ console.warn(`[w5-consent] ignoring ?${CONSENT_OVERRIDE_QUERY_PARAM}=${raw} — only "denied" is accepted from a URL off a local origin. ` + `Set localStorage["${CONSENT_OVERRIDE_KEY}"] = "${raw}" instead.`);
57
+ return null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+ const readStorage = () => {
63
+ try {
64
+ const raw = localStorage.getItem(CONSENT_OVERRIDE_KEY);
65
+ return isConsentState(raw) ? raw : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+
71
+ /** The forced consent state, or `null` when no override is active. */
72
+ const getConsentOverride = () => readQueryParam() ?? readStorage();
73
+ exports.getConsentOverride = getConsentOverride;
74
+ const overrideSnapshot = state => ({
75
+ analytics: state,
76
+ marketing: state,
77
+ performance: state
78
+ });
79
+
80
+ /** Persist the override and apply it on the next load. `null` clears it. */
81
+ exports.overrideSnapshot = overrideSnapshot;
82
+ const setConsentOverride = state => {
83
+ try {
84
+ if (state) {
85
+ localStorage.setItem(CONSENT_OVERRIDE_KEY, state);
86
+ } else {
87
+ localStorage.removeItem(CONSENT_OVERRIDE_KEY);
88
+ }
89
+ } catch {
90
+ // Ignore — the override simply won't persist.
91
+ }
92
+ };
93
+ exports.setConsentOverride = setConsentOverride;
94
+ //# sourceMappingURL=consentOverride.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["CONSENT_OVERRIDE_KEY","exports","CONSENT_OVERRIDE_QUERY_PARAM","VALID","isConsentState","value","includes","isLocalOrigin","hostname","window","location","endsWith","readQueryParam","raw","URLSearchParams","search","get","console","warn","readStorage","localStorage","getItem","getConsentOverride","overrideSnapshot","state","analytics","marketing","performance","setConsentOverride","setItem","removeItem"],"sources":["../../../src/privacy/consentOverride.ts"],"sourcesContent":["/**\n * Consent override (debug-only) — DL #218.\n *\n * Shopify only serves its cookie banner where consent is legally required, so\n * from most of the world `shouldShowBanner()` is `false` and there is no way to\n * reach the `unknown` or `denied` branches by hand. Development stores often\n * have no privacy configuration at all, which pins the gate at `unknown`\n * forever. Neither is a bug, and both make the interesting states unreachable\n * from a browser.\n *\n * This forces the gate's answer so all four rows of the truth table can be\n * walked on any store, in any region, published or not.\n *\n * ?w5consent=granted | denied | unknown (one-shot)\n * localStorage[\"w5_consent_override\"] (sticky)\n *\n * **The two are not equally trusted, on purpose.** A link is something one\n * person can send another, so a query param may only ever *reduce* what is\n * collected: `denied` is honoured anywhere, while `granted` and `unknown` are\n * honoured only on a local origin. Turning tracking *on* for someone else by\n * sending them a URL is exactly the thing this whole feature exists to\n * prevent. On a real storefront, set the localStorage key in devtools — which\n * is a deliberate act by the person whose browser it is.\n */\n\nimport type { ConsentSnapshot, ConsentState } from './types';\n\nexport const CONSENT_OVERRIDE_KEY = 'w5_consent_override';\nexport const CONSENT_OVERRIDE_QUERY_PARAM = 'w5consent';\n\nconst VALID: readonly ConsentState[] = ['granted', 'denied', 'unknown'];\n\nconst isConsentState = (value: unknown): value is ConsentState =>\n typeof value === 'string' && (VALID as readonly string[]).includes(value);\n\n/** Only a local origin may force a *more* permissive state from a URL. */\nconst isLocalOrigin = (): boolean => {\n try {\n const { hostname } = window.location;\n return (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname.endsWith('.local')\n );\n } catch {\n return false;\n }\n};\n\nconst readQueryParam = (): ConsentState | null => {\n try {\n const raw = new URLSearchParams(window.location.search).get(\n CONSENT_OVERRIDE_QUERY_PARAM,\n );\n if (!isConsentState(raw)) {\n return null;\n }\n // `denied` can only ever collect less, so a link may set it anywhere.\n if (raw === 'denied' || isLocalOrigin()) {\n return raw;\n }\n console.warn(\n `[w5-consent] ignoring ?${CONSENT_OVERRIDE_QUERY_PARAM}=${raw} — only \"denied\" is accepted from a URL off a local origin. ` +\n `Set localStorage[\"${CONSENT_OVERRIDE_KEY}\"] = \"${raw}\" instead.`,\n );\n return null;\n } catch {\n return null;\n }\n};\n\nconst readStorage = (): ConsentState | null => {\n try {\n const raw = localStorage.getItem(CONSENT_OVERRIDE_KEY);\n return isConsentState(raw) ? raw : null;\n } catch {\n return null;\n }\n};\n\n/** The forced consent state, or `null` when no override is active. */\nexport const getConsentOverride = (): ConsentState | null =>\n readQueryParam() ?? readStorage();\n\nexport const overrideSnapshot = (state: ConsentState): ConsentSnapshot => ({\n analytics: state,\n marketing: state,\n performance: state,\n});\n\n/** Persist the override and apply it on the next load. `null` clears it. */\nexport const setConsentOverride = (state: ConsentState | null): void => {\n try {\n if (state) {\n localStorage.setItem(CONSENT_OVERRIDE_KEY, state);\n } else {\n localStorage.removeItem(CONSENT_OVERRIDE_KEY);\n }\n } catch {\n // Ignore — the override simply won't persist.\n }\n};\n"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIO,MAAMA,oBAAoB,GAAAC,OAAA,CAAAD,oBAAA,GAAG,qBAAqB;AAClD,MAAME,4BAA4B,GAAAD,OAAA,CAAAC,4BAAA,GAAG,WAAW;AAEvD,MAAMC,KAA8B,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC;AAEvE,MAAMC,cAAc,GAAIC,KAAc,IACpC,OAAOA,KAAK,KAAK,QAAQ,IAAKF,KAAK,CAAuBG,QAAQ,CAACD,KAAK,CAAC;;AAE3E;AACA,MAAME,aAAa,GAAGA,CAAA,KAAe;EACnC,IAAI;IACF,MAAM;MAAEC;IAAS,CAAC,GAAGC,MAAM,CAACC,QAAQ;IACpC,OACEF,QAAQ,KAAK,WAAW,IACxBA,QAAQ,KAAK,WAAW,IACxBA,QAAQ,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAE/B,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;AACF,CAAC;AAED,MAAMC,cAAc,GAAGA,CAAA,KAA2B;EAChD,IAAI;IACF,MAAMC,GAAG,GAAG,IAAIC,eAAe,CAACL,MAAM,CAACC,QAAQ,CAACK,MAAM,CAAC,CAACC,GAAG,CACzDd,4BACF,CAAC;IACD,IAAI,CAACE,cAAc,CAACS,GAAG,CAAC,EAAE;MACxB,OAAO,IAAI;IACb;IACA;IACA,IAAIA,GAAG,KAAK,QAAQ,IAAIN,aAAa,CAAC,CAAC,EAAE;MACvC,OAAOM,GAAG;IACZ;IACAI,OAAO,CAACC,IAAI,CACV,0BAA0BhB,4BAA4B,IAAIW,GAAG,8DAA8D,GACzH,qBAAqBb,oBAAoB,SAASa,GAAG,YACzD,CAAC;IACD,OAAO,IAAI;EACb,CAAC,CAAC,MAAM;IACN,OAAO,IAAI;EACb;AACF,CAAC;AAED,MAAMM,WAAW,GAAGA,CAAA,KAA2B;EAC7C,IAAI;IACF,MAAMN,GAAG,GAAGO,YAAY,CAACC,OAAO,CAACrB,oBAAoB,CAAC;IACtD,OAAOI,cAAc,CAACS,GAAG,CAAC,GAAGA,GAAG,GAAG,IAAI;EACzC,CAAC,CAAC,MAAM;IACN,OAAO,IAAI;EACb;AACF,CAAC;;AAED;AACO,MAAMS,kBAAkB,GAAGA,CAAA,KAChCV,cAAc,CAAC,CAAC,IAAIO,WAAW,CAAC,CAAC;AAAClB,OAAA,CAAAqB,kBAAA,GAAAA,kBAAA;AAE7B,MAAMC,gBAAgB,GAAIC,KAAmB,KAAuB;EACzEC,SAAS,EAAED,KAAK;EAChBE,SAAS,EAAEF,KAAK;EAChBG,WAAW,EAAEH;AACf,CAAC,CAAC;;AAEF;AAAAvB,OAAA,CAAAsB,gBAAA,GAAAA,gBAAA;AACO,MAAMK,kBAAkB,GAAIJ,KAA0B,IAAW;EACtE,IAAI;IACF,IAAIA,KAAK,EAAE;MACTJ,YAAY,CAACS,OAAO,CAAC7B,oBAAoB,EAAEwB,KAAK,CAAC;IACnD,CAAC,MAAM;MACLJ,YAAY,CAACU,UAAU,CAAC9B,oBAAoB,CAAC;IAC/C;EACF,CAAC,CAAC,MAAM;IACN;EAAA;AAEJ,CAAC;AAACC,OAAA,CAAA2B,kBAAA,GAAAA,kBAAA","ignoreList":[]}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.initConsentGate = exports.detectConsentProvider = void 0;
5
+ var _consentGate = require("./consentGate");
6
+ var _consentOverride = require("./consentOverride");
7
+ var _hostSuppliedProvider = require("./providers/hostSuppliedProvider");
8
+ var _shopifyProvider = require("./providers/shopifyProvider");
9
+ var _oneTrustProvider = require("./providers/oneTrustProvider");
10
+ /**
11
+ * Provider selection — DL #218.
12
+ *
13
+ * Detected at boot, not configured per client, because a merchant can install
14
+ * a CMP long after we deploy and the bundle is not the source of truth for a
15
+ * store's behaviour (DL #214).
16
+ *
17
+ * Order is host-supplied → Shopify → OneTrust → none. A host that publishes an
18
+ * answer outranks anything we could sniff, because it knows things we cannot:
19
+ * a server-side consent record, a CMP behind its own abstraction, or a legal
20
+ * position we have no business guessing at.
21
+ *
22
+ * When nothing is detected the gate keeps its default — everything `unknown`,
23
+ * held until the visitor acts. That is the case on every Shopify store with no
24
+ * privacy configuration, which today is most of them.
25
+ */
26
+
27
+ const detectConsentProvider = () => {
28
+ // Debug override outranks every real signal, so the states a region or an
29
+ // unconfigured store makes unreachable can still be walked by hand.
30
+ const forced = (0, _consentOverride.getConsentOverride)();
31
+ if (forced) {
32
+ console.warn(`[w5-consent] OVERRIDE ACTIVE — consent forced to "${forced}". This is a debug switch, not a real answer.`);
33
+ return {
34
+ name: `override:${forced}`,
35
+ read: () => (0, _consentOverride.overrideSnapshot)(forced),
36
+ subscribe: () => () => {}
37
+ };
38
+ }
39
+ if ((0, _hostSuppliedProvider.hasHostSuppliedConsent)()) {
40
+ return (0, _hostSuppliedProvider.createHostSuppliedConsentProvider)();
41
+ }
42
+ if ((0, _shopifyProvider.isShopifyHost)()) {
43
+ return (0, _shopifyProvider.createShopifyConsentProvider)();
44
+ }
45
+ if ((0, _oneTrustProvider.isOneTrustHost)()) {
46
+ return (0, _oneTrustProvider.createOneTrustConsentProvider)();
47
+ }
48
+ return null;
49
+ };
50
+
51
+ /**
52
+ * Install the detected provider, if any. Call once at boot, before the first
53
+ * emitter runs — anything raised earlier is held rather than lost, but the
54
+ * sooner this runs the less the buffer has to carry.
55
+ */
56
+ exports.detectConsentProvider = detectConsentProvider;
57
+ const initConsentGate = () => {
58
+ const provider = detectConsentProvider();
59
+ if (provider) {
60
+ (0, _consentGate.installConsentProvider)(provider);
61
+ } else {
62
+ try {
63
+ console.log('[w5-consent] no CMP detected (no host-supplied consent, no Shopify customerPrivacy, no OneTrust) — everything held until the visitor submits a prompt');
64
+ } catch {
65
+ /* never break on a console */
66
+ }
67
+ }
68
+ return provider;
69
+ };
70
+ exports.initConsentGate = initConsentGate;
71
+ //# sourceMappingURL=detectProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_consentGate","require","_consentOverride","_hostSuppliedProvider","_shopifyProvider","_oneTrustProvider","detectConsentProvider","forced","getConsentOverride","console","warn","name","read","overrideSnapshot","subscribe","hasHostSuppliedConsent","createHostSuppliedConsentProvider","isShopifyHost","createShopifyConsentProvider","isOneTrustHost","createOneTrustConsentProvider","exports","initConsentGate","provider","installConsentProvider","log"],"sources":["../../../src/privacy/detectProvider.ts"],"sourcesContent":["/**\n * Provider selection — DL #218.\n *\n * Detected at boot, not configured per client, because a merchant can install\n * a CMP long after we deploy and the bundle is not the source of truth for a\n * store's behaviour (DL #214).\n *\n * Order is host-supplied → Shopify → OneTrust → none. A host that publishes an\n * answer outranks anything we could sniff, because it knows things we cannot:\n * a server-side consent record, a CMP behind its own abstraction, or a legal\n * position we have no business guessing at.\n *\n * When nothing is detected the gate keeps its default — everything `unknown`,\n * held until the visitor acts. That is the case on every Shopify store with no\n * privacy configuration, which today is most of them.\n */\n\nimport { installConsentProvider } from './consentGate';\nimport { getConsentOverride, overrideSnapshot } from './consentOverride';\nimport type { ConsentProvider } from './types';\nimport {\n createHostSuppliedConsentProvider,\n hasHostSuppliedConsent,\n} from './providers/hostSuppliedProvider';\nimport {\n createShopifyConsentProvider,\n isShopifyHost,\n} from './providers/shopifyProvider';\nimport {\n createOneTrustConsentProvider,\n isOneTrustHost,\n} from './providers/oneTrustProvider';\n\nexport const detectConsentProvider = (): ConsentProvider | null => {\n // Debug override outranks every real signal, so the states a region or an\n // unconfigured store makes unreachable can still be walked by hand.\n const forced = getConsentOverride();\n if (forced) {\n console.warn(\n `[w5-consent] OVERRIDE ACTIVE — consent forced to \"${forced}\". This is a debug switch, not a real answer.`,\n );\n return {\n name: `override:${forced}`,\n read: () => overrideSnapshot(forced),\n subscribe: () => () => {},\n };\n }\n if (hasHostSuppliedConsent()) {\n return createHostSuppliedConsentProvider();\n }\n if (isShopifyHost()) {\n return createShopifyConsentProvider();\n }\n if (isOneTrustHost()) {\n return createOneTrustConsentProvider();\n }\n return null;\n};\n\n/**\n * Install the detected provider, if any. Call once at boot, before the first\n * emitter runs — anything raised earlier is held rather than lost, but the\n * sooner this runs the less the buffer has to carry.\n */\nexport const initConsentGate = (): ConsentProvider | null => {\n const provider = detectConsentProvider();\n if (provider) {\n installConsentProvider(provider);\n } else {\n try {\n console.log(\n '[w5-consent] no CMP detected (no host-supplied consent, no Shopify customerPrivacy, no OneTrust) — everything held until the visitor submits a prompt',\n );\n } catch {\n /* never break on a console */\n }\n }\n return provider;\n};\n"],"mappings":";;;;AAiBA,IAAAA,YAAA,GAAAC,OAAA;AACA,IAAAC,gBAAA,GAAAD,OAAA;AAEA,IAAAE,qBAAA,GAAAF,OAAA;AAIA,IAAAG,gBAAA,GAAAH,OAAA;AAIA,IAAAI,iBAAA,GAAAJ,OAAA;AA5BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAkBO,MAAMK,qBAAqB,GAAGA,CAAA,KAA8B;EACjE;EACA;EACA,MAAMC,MAAM,GAAG,IAAAC,mCAAkB,EAAC,CAAC;EACnC,IAAID,MAAM,EAAE;IACVE,OAAO,CAACC,IAAI,CACV,qDAAqDH,MAAM,+CAC7D,CAAC;IACD,OAAO;MACLI,IAAI,EAAE,YAAYJ,MAAM,EAAE;MAC1BK,IAAI,EAAEA,CAAA,KAAM,IAAAC,iCAAgB,EAACN,MAAM,CAAC;MACpCO,SAAS,EAAEA,CAAA,KAAM,MAAM,CAAC;IAC1B,CAAC;EACH;EACA,IAAI,IAAAC,4CAAsB,EAAC,CAAC,EAAE;IAC5B,OAAO,IAAAC,uDAAiC,EAAC,CAAC;EAC5C;EACA,IAAI,IAAAC,8BAAa,EAAC,CAAC,EAAE;IACnB,OAAO,IAAAC,6CAA4B,EAAC,CAAC;EACvC;EACA,IAAI,IAAAC,gCAAc,EAAC,CAAC,EAAE;IACpB,OAAO,IAAAC,+CAA6B,EAAC,CAAC;EACxC;EACA,OAAO,IAAI;AACb,CAAC;;AAED;AACA;AACA;AACA;AACA;AAJAC,OAAA,CAAAf,qBAAA,GAAAA,qBAAA;AAKO,MAAMgB,eAAe,GAAGA,CAAA,KAA8B;EAC3D,MAAMC,QAAQ,GAAGjB,qBAAqB,CAAC,CAAC;EACxC,IAAIiB,QAAQ,EAAE;IACZ,IAAAC,mCAAsB,EAACD,QAAQ,CAAC;EAClC,CAAC,MAAM;IACL,IAAI;MACFd,OAAO,CAACgB,GAAG,CACT,uJACF,CAAC;IACH,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;EACA,OAAOF,QAAQ;AACjB,CAAC;AAACF,OAAA,CAAAC,eAAA,GAAAA,eAAA","ignoreList":[]}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.unlockOnUserAction = exports.transmit = exports.subscribeToConsent = exports.setConsentOverride = exports.setConsentBufferLimit = exports.resetConsentGateForTests = exports.publishHostConsent = exports.mayTransmit = exports.mayPersistIdentity = exports.isUserEngaged = exports.isShopifyHost = exports.isOneTrustHost = exports.installConsentProvider = exports.initConsentGate = exports.hasHostSuppliedConsent = exports.getInstalledProviderName = exports.getGateView = exports.getConsentSnapshot = exports.getConsentOverride = exports.getConsentGateStats = exports.detectConsentProvider = exports.createShopifyConsentProvider = exports.createOneTrustConsentProvider = exports.createHostSuppliedConsentProvider = exports.UNKNOWN_CONSENT = exports.HOST_CONSENT_GLOBAL = exports.CONSENT_OVERRIDE_QUERY_PARAM = exports.CONSENT_OVERRIDE_KEY = void 0;
5
+ var _types = require("./types");
6
+ exports.UNKNOWN_CONSENT = _types.UNKNOWN_CONSENT;
7
+ var _consentGate = require("./consentGate");
8
+ exports.transmit = _consentGate.transmit;
9
+ exports.mayTransmit = _consentGate.mayTransmit;
10
+ exports.unlockOnUserAction = _consentGate.unlockOnUserAction;
11
+ exports.isUserEngaged = _consentGate.isUserEngaged;
12
+ exports.mayPersistIdentity = _consentGate.mayPersistIdentity;
13
+ exports.getConsentSnapshot = _consentGate.getConsentSnapshot;
14
+ exports.getGateView = _consentGate.getGateView;
15
+ exports.subscribeToConsent = _consentGate.subscribeToConsent;
16
+ exports.installConsentProvider = _consentGate.installConsentProvider;
17
+ exports.getInstalledProviderName = _consentGate.getInstalledProviderName;
18
+ exports.setConsentBufferLimit = _consentGate.setConsentBufferLimit;
19
+ exports.getConsentGateStats = _consentGate.getConsentGateStats;
20
+ exports.resetConsentGateForTests = _consentGate.resetConsentGateForTests;
21
+ var _detectProvider = require("./detectProvider");
22
+ exports.detectConsentProvider = _detectProvider.detectConsentProvider;
23
+ exports.initConsentGate = _detectProvider.initConsentGate;
24
+ var _consentOverride = require("./consentOverride");
25
+ exports.CONSENT_OVERRIDE_KEY = _consentOverride.CONSENT_OVERRIDE_KEY;
26
+ exports.CONSENT_OVERRIDE_QUERY_PARAM = _consentOverride.CONSENT_OVERRIDE_QUERY_PARAM;
27
+ exports.getConsentOverride = _consentOverride.getConsentOverride;
28
+ exports.setConsentOverride = _consentOverride.setConsentOverride;
29
+ var _shopifyProvider = require("./providers/shopifyProvider");
30
+ exports.createShopifyConsentProvider = _shopifyProvider.createShopifyConsentProvider;
31
+ exports.isShopifyHost = _shopifyProvider.isShopifyHost;
32
+ var _oneTrustProvider = require("./providers/oneTrustProvider");
33
+ exports.createOneTrustConsentProvider = _oneTrustProvider.createOneTrustConsentProvider;
34
+ exports.isOneTrustHost = _oneTrustProvider.isOneTrustHost;
35
+ var _hostSuppliedProvider = require("./providers/hostSuppliedProvider");
36
+ exports.HOST_CONSENT_GLOBAL = _hostSuppliedProvider.HOST_CONSENT_GLOBAL;
37
+ exports.createHostSuppliedConsentProvider = _hostSuppliedProvider.createHostSuppliedConsentProvider;
38
+ exports.hasHostSuppliedConsent = _hostSuppliedProvider.hasHostSuppliedConsent;
39
+ exports.publishHostConsent = _hostSuppliedProvider.publishHostConsent;
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_types","require","exports","UNKNOWN_CONSENT","_consentGate","transmit","mayTransmit","unlockOnUserAction","isUserEngaged","mayPersistIdentity","getConsentSnapshot","getGateView","subscribeToConsent","installConsentProvider","getInstalledProviderName","setConsentBufferLimit","getConsentGateStats","resetConsentGateForTests","_detectProvider","detectConsentProvider","initConsentGate","_consentOverride","CONSENT_OVERRIDE_KEY","CONSENT_OVERRIDE_QUERY_PARAM","getConsentOverride","setConsentOverride","_shopifyProvider","createShopifyConsentProvider","isShopifyHost","_oneTrustProvider","createOneTrustConsentProvider","isOneTrustHost","_hostSuppliedProvider","HOST_CONSENT_GLOBAL","createHostSuppliedConsentProvider","hasHostSuppliedConsent","publishHostConsent"],"sources":["../../../src/privacy/index.ts"],"sourcesContent":["export {\n type ConsentState,\n type ConsentPurpose,\n type GatedPurpose,\n type ConsentSnapshot,\n type ConsentProvider,\n UNKNOWN_CONSENT,\n} from './types';\n\nexport {\n type GateView,\n transmit,\n mayTransmit,\n unlockOnUserAction,\n isUserEngaged,\n mayPersistIdentity,\n getConsentSnapshot,\n getGateView,\n subscribeToConsent,\n installConsentProvider,\n getInstalledProviderName,\n setConsentBufferLimit,\n getConsentGateStats,\n resetConsentGateForTests,\n} from './consentGate';\n\nexport { detectConsentProvider, initConsentGate } from './detectProvider';\n\nexport {\n CONSENT_OVERRIDE_KEY,\n CONSENT_OVERRIDE_QUERY_PARAM,\n getConsentOverride,\n setConsentOverride,\n} from './consentOverride';\n\nexport {\n createShopifyConsentProvider,\n isShopifyHost,\n} from './providers/shopifyProvider';\nexport {\n createOneTrustConsentProvider,\n isOneTrustHost,\n} from './providers/oneTrustProvider';\nexport {\n type HostConsentInput,\n HOST_CONSENT_GLOBAL,\n createHostSuppliedConsentProvider,\n hasHostSuppliedConsent,\n publishHostConsent,\n} from './providers/hostSuppliedProvider';\n"],"mappings":";;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAOiBC,OAAA,CAAAC,eAAA,GAAAH,MAAA,CAAAG,eAAA;AAEjB,IAAAC,YAAA,GAAAH,OAAA;AAeuBC,OAAA,CAAAG,QAAA,GAAAD,YAAA,CAAAC,QAAA;AAAAH,OAAA,CAAAI,WAAA,GAAAF,YAAA,CAAAE,WAAA;AAAAJ,OAAA,CAAAK,kBAAA,GAAAH,YAAA,CAAAG,kBAAA;AAAAL,OAAA,CAAAM,aAAA,GAAAJ,YAAA,CAAAI,aAAA;AAAAN,OAAA,CAAAO,kBAAA,GAAAL,YAAA,CAAAK,kBAAA;AAAAP,OAAA,CAAAQ,kBAAA,GAAAN,YAAA,CAAAM,kBAAA;AAAAR,OAAA,CAAAS,WAAA,GAAAP,YAAA,CAAAO,WAAA;AAAAT,OAAA,CAAAU,kBAAA,GAAAR,YAAA,CAAAQ,kBAAA;AAAAV,OAAA,CAAAW,sBAAA,GAAAT,YAAA,CAAAS,sBAAA;AAAAX,OAAA,CAAAY,wBAAA,GAAAV,YAAA,CAAAU,wBAAA;AAAAZ,OAAA,CAAAa,qBAAA,GAAAX,YAAA,CAAAW,qBAAA;AAAAb,OAAA,CAAAc,mBAAA,GAAAZ,YAAA,CAAAY,mBAAA;AAAAd,OAAA,CAAAe,wBAAA,GAAAb,YAAA,CAAAa,wBAAA;AAEvB,IAAAC,eAAA,GAAAjB,OAAA;AAA0EC,OAAA,CAAAiB,qBAAA,GAAAD,eAAA,CAAAC,qBAAA;AAAAjB,OAAA,CAAAkB,eAAA,GAAAF,eAAA,CAAAE,eAAA;AAE1E,IAAAC,gBAAA,GAAApB,OAAA;AAK2BC,OAAA,CAAAoB,oBAAA,GAAAD,gBAAA,CAAAC,oBAAA;AAAApB,OAAA,CAAAqB,4BAAA,GAAAF,gBAAA,CAAAE,4BAAA;AAAArB,OAAA,CAAAsB,kBAAA,GAAAH,gBAAA,CAAAG,kBAAA;AAAAtB,OAAA,CAAAuB,kBAAA,GAAAJ,gBAAA,CAAAI,kBAAA;AAE3B,IAAAC,gBAAA,GAAAzB,OAAA;AAGqCC,OAAA,CAAAyB,4BAAA,GAAAD,gBAAA,CAAAC,4BAAA;AAAAzB,OAAA,CAAA0B,aAAA,GAAAF,gBAAA,CAAAE,aAAA;AACrC,IAAAC,iBAAA,GAAA5B,OAAA;AAGsCC,OAAA,CAAA4B,6BAAA,GAAAD,iBAAA,CAAAC,6BAAA;AAAA5B,OAAA,CAAA6B,cAAA,GAAAF,iBAAA,CAAAE,cAAA;AACtC,IAAAC,qBAAA,GAAA/B,OAAA;AAM0CC,OAAA,CAAA+B,mBAAA,GAAAD,qBAAA,CAAAC,mBAAA;AAAA/B,OAAA,CAAAgC,iCAAA,GAAAF,qBAAA,CAAAE,iCAAA;AAAAhC,OAAA,CAAAiC,sBAAA,GAAAH,qBAAA,CAAAG,sBAAA;AAAAjC,OAAA,CAAAkC,kBAAA,GAAAJ,qBAAA,CAAAI,kBAAA","ignoreList":[]}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.publishHostConsent = exports.hasHostSuppliedConsent = exports.createHostSuppliedConsentProvider = exports.__resetHostConsentForTests = exports.HOST_CONSENT_GLOBAL = void 0;
5
+ var _types = require("../types");
6
+ /**
7
+ * Host-supplied provider — DL #218.
8
+ *
9
+ * The escape hatch for every CMP we cannot detect: a host page that already
10
+ * knows its visitor's answer publishes it, and the gate believes it. This is
11
+ * how Circana, feature.com and any merchant on Cookiebot/Osano/Klaviyo reach
12
+ * the gate without web5 learning each vendor's API.
13
+ *
14
+ * Two ways in, because hosts differ in when they know:
15
+ * - `window.__web5_consent__` set before the bundle loads, read at install
16
+ * - `publishHostConsent()` called at any time afterwards
17
+ */
18
+
19
+ const HOST_CONSENT_GLOBAL = exports.HOST_CONSENT_GLOBAL = '__web5_consent__';
20
+
21
+ /** What a host may publish. Anything missing stays `unknown`. */
22
+
23
+ const coerce = value => {
24
+ if (value === true) return 'granted';
25
+ if (value === false) return 'denied';
26
+ if (value === 'granted' || value === 'denied' || value === 'unknown') {
27
+ return value;
28
+ }
29
+ return 'unknown';
30
+ };
31
+ const normalize = input => {
32
+ if (!input || typeof input !== 'object') {
33
+ return _types.UNKNOWN_CONSENT;
34
+ }
35
+ const analytics = coerce(input.analytics);
36
+ return {
37
+ analytics,
38
+ marketing: coerce(input.marketing),
39
+ // A host that speaks only about analytics is taken to mean the same for
40
+ // load telemetry, which is the same wire and the same recipient.
41
+ performance: input.performance === undefined ? analytics : coerce(input.performance)
42
+ };
43
+ };
44
+ const readGlobal = () => {
45
+ if (typeof window === 'undefined') {
46
+ return _types.UNKNOWN_CONSENT;
47
+ }
48
+ const raw = window[HOST_CONSENT_GLOBAL];
49
+ return normalize(raw);
50
+ };
51
+ const hasHostSuppliedConsent = () => {
52
+ if (typeof window === 'undefined') {
53
+ return false;
54
+ }
55
+ const raw = window[HOST_CONSENT_GLOBAL];
56
+ return !!raw && typeof raw === 'object';
57
+ };
58
+ exports.hasHostSuppliedConsent = hasHostSuppliedConsent;
59
+ const subscribers = new Set();
60
+ let published = null;
61
+
62
+ /**
63
+ * Called by the host — directly, or by the loader when it is handed consent in
64
+ * its boot options — whenever the visitor's answer is known or changes.
65
+ */
66
+ const publishHostConsent = input => {
67
+ published = normalize(input);
68
+ for (const subscriber of subscribers) {
69
+ subscriber(published);
70
+ }
71
+ };
72
+ exports.publishHostConsent = publishHostConsent;
73
+ const createHostSuppliedConsentProvider = () => ({
74
+ name: 'host-supplied',
75
+ read: () => published ?? readGlobal(),
76
+ subscribe(onChange) {
77
+ subscribers.add(onChange);
78
+ return () => {
79
+ subscribers.delete(onChange);
80
+ };
81
+ }
82
+ });
83
+ exports.createHostSuppliedConsentProvider = createHostSuppliedConsentProvider;
84
+ const __resetHostConsentForTests = () => {
85
+ published = null;
86
+ subscribers.clear();
87
+ };
88
+ exports.__resetHostConsentForTests = __resetHostConsentForTests;
89
+ //# sourceMappingURL=hostSuppliedProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_types","require","HOST_CONSENT_GLOBAL","exports","coerce","value","normalize","input","UNKNOWN_CONSENT","analytics","marketing","performance","undefined","readGlobal","window","raw","hasHostSuppliedConsent","subscribers","Set","published","publishHostConsent","subscriber","createHostSuppliedConsentProvider","name","read","subscribe","onChange","add","delete","__resetHostConsentForTests","clear"],"sources":["../../../../src/privacy/providers/hostSuppliedProvider.ts"],"sourcesContent":["/**\n * Host-supplied provider — DL #218.\n *\n * The escape hatch for every CMP we cannot detect: a host page that already\n * knows its visitor's answer publishes it, and the gate believes it. This is\n * how Circana, feature.com and any merchant on Cookiebot/Osano/Klaviyo reach\n * the gate without web5 learning each vendor's API.\n *\n * Two ways in, because hosts differ in when they know:\n * - `window.__web5_consent__` set before the bundle loads, read at install\n * - `publishHostConsent()` called at any time afterwards\n */\n\nimport {\n UNKNOWN_CONSENT,\n type ConsentProvider,\n type ConsentSnapshot,\n type ConsentState,\n} from '../types';\n\nexport const HOST_CONSENT_GLOBAL = '__web5_consent__';\n\n/** What a host may publish. Anything missing stays `unknown`. */\nexport interface HostConsentInput {\n analytics?: ConsentState | boolean;\n marketing?: ConsentState | boolean;\n performance?: ConsentState | boolean;\n}\n\nconst coerce = (value: ConsentState | boolean | undefined): ConsentState => {\n if (value === true) return 'granted';\n if (value === false) return 'denied';\n if (value === 'granted' || value === 'denied' || value === 'unknown') {\n return value;\n }\n return 'unknown';\n};\n\nconst normalize = (input: HostConsentInput | null | undefined): ConsentSnapshot => {\n if (!input || typeof input !== 'object') {\n return UNKNOWN_CONSENT;\n }\n const analytics = coerce(input.analytics);\n return {\n analytics,\n marketing: coerce(input.marketing),\n // A host that speaks only about analytics is taken to mean the same for\n // load telemetry, which is the same wire and the same recipient.\n performance:\n input.performance === undefined ? analytics : coerce(input.performance),\n };\n};\n\nconst readGlobal = (): ConsentSnapshot => {\n if (typeof window === 'undefined') {\n return UNKNOWN_CONSENT;\n }\n const raw = (window as unknown as Record<string, unknown>)[HOST_CONSENT_GLOBAL];\n return normalize(raw as HostConsentInput | undefined);\n};\n\nexport const hasHostSuppliedConsent = (): boolean => {\n if (typeof window === 'undefined') {\n return false;\n }\n const raw = (window as unknown as Record<string, unknown>)[HOST_CONSENT_GLOBAL];\n return !!raw && typeof raw === 'object';\n};\n\nconst subscribers = new Set<(snapshot: ConsentSnapshot) => void>();\nlet published: ConsentSnapshot | null = null;\n\n/**\n * Called by the host — directly, or by the loader when it is handed consent in\n * its boot options — whenever the visitor's answer is known or changes.\n */\nexport const publishHostConsent = (input: HostConsentInput): void => {\n published = normalize(input);\n for (const subscriber of subscribers) {\n subscriber(published);\n }\n};\n\nexport const createHostSuppliedConsentProvider = (): ConsentProvider => ({\n name: 'host-supplied',\n\n read: () => published ?? readGlobal(),\n\n subscribe(onChange) {\n subscribers.add(onChange);\n return () => {\n subscribers.delete(onChange);\n };\n },\n});\n\nexport const __resetHostConsentForTests = (): void => {\n published = null;\n subscribers.clear();\n};\n"],"mappings":";;;;AAaA,IAAAA,MAAA,GAAAC,OAAA;AAbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AASO,MAAMC,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,GAAG,kBAAkB;;AAErD;;AAOA,MAAME,MAAM,GAAIC,KAAyC,IAAmB;EAC1E,IAAIA,KAAK,KAAK,IAAI,EAAE,OAAO,SAAS;EACpC,IAAIA,KAAK,KAAK,KAAK,EAAE,OAAO,QAAQ;EACpC,IAAIA,KAAK,KAAK,SAAS,IAAIA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,SAAS,EAAE;IACpE,OAAOA,KAAK;EACd;EACA,OAAO,SAAS;AAClB,CAAC;AAED,MAAMC,SAAS,GAAIC,KAA0C,IAAsB;EACjF,IAAI,CAACA,KAAK,IAAI,OAAOA,KAAK,KAAK,QAAQ,EAAE;IACvC,OAAOC,sBAAe;EACxB;EACA,MAAMC,SAAS,GAAGL,MAAM,CAACG,KAAK,CAACE,SAAS,CAAC;EACzC,OAAO;IACLA,SAAS;IACTC,SAAS,EAAEN,MAAM,CAACG,KAAK,CAACG,SAAS,CAAC;IAClC;IACA;IACAC,WAAW,EACTJ,KAAK,CAACI,WAAW,KAAKC,SAAS,GAAGH,SAAS,GAAGL,MAAM,CAACG,KAAK,CAACI,WAAW;EAC1E,CAAC;AACH,CAAC;AAED,MAAME,UAAU,GAAGA,CAAA,KAAuB;EACxC,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,OAAON,sBAAe;EACxB;EACA,MAAMO,GAAG,GAAID,MAAM,CAAwCZ,mBAAmB,CAAC;EAC/E,OAAOI,SAAS,CAACS,GAAmC,CAAC;AACvD,CAAC;AAEM,MAAMC,sBAAsB,GAAGA,CAAA,KAAe;EACnD,IAAI,OAAOF,MAAM,KAAK,WAAW,EAAE;IACjC,OAAO,KAAK;EACd;EACA,MAAMC,GAAG,GAAID,MAAM,CAAwCZ,mBAAmB,CAAC;EAC/E,OAAO,CAAC,CAACa,GAAG,IAAI,OAAOA,GAAG,KAAK,QAAQ;AACzC,CAAC;AAACZ,OAAA,CAAAa,sBAAA,GAAAA,sBAAA;AAEF,MAAMC,WAAW,GAAG,IAAIC,GAAG,CAAsC,CAAC;AAClE,IAAIC,SAAiC,GAAG,IAAI;;AAE5C;AACA;AACA;AACA;AACO,MAAMC,kBAAkB,GAAIb,KAAuB,IAAW;EACnEY,SAAS,GAAGb,SAAS,CAACC,KAAK,CAAC;EAC5B,KAAK,MAAMc,UAAU,IAAIJ,WAAW,EAAE;IACpCI,UAAU,CAACF,SAAS,CAAC;EACvB;AACF,CAAC;AAAChB,OAAA,CAAAiB,kBAAA,GAAAA,kBAAA;AAEK,MAAME,iCAAiC,GAAGA,CAAA,MAAwB;EACvEC,IAAI,EAAE,eAAe;EAErBC,IAAI,EAAEA,CAAA,KAAML,SAAS,IAAIN,UAAU,CAAC,CAAC;EAErCY,SAASA,CAACC,QAAQ,EAAE;IAClBT,WAAW,CAACU,GAAG,CAACD,QAAQ,CAAC;IACzB,OAAO,MAAM;MACXT,WAAW,CAACW,MAAM,CAACF,QAAQ,CAAC;IAC9B,CAAC;EACH;AACF,CAAC,CAAC;AAACvB,OAAA,CAAAmB,iCAAA,GAAAA,iCAAA;AAEI,MAAMO,0BAA0B,GAAGA,CAAA,KAAY;EACpDV,SAAS,GAAG,IAAI;EAChBF,WAAW,CAACa,KAAK,CAAC,CAAC;AACrB,CAAC;AAAC3B,OAAA,CAAA0B,0BAAA,GAAAA,0BAAA","ignoreList":[]}