@createcms/core 0.1.1
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/README.md +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
/** Default-deny: nothing is granted until a CMP / Consent Mode signal says so. */ const DENIED_ALL = {
|
|
7
|
+
analytics_storage: 'denied',
|
|
8
|
+
ad_storage: 'denied',
|
|
9
|
+
ad_user_data: 'denied',
|
|
10
|
+
ad_personalization: 'denied'
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* How long (ms) to buffer events before resolving the gate when no consent
|
|
14
|
+
* DECISION has arrived yet — the Consent Mode `wait_for_update` window. Render
|
|
15
|
+
* is NEVER blocked on this; only event emission waits.
|
|
16
|
+
*/ const CONSENT_WAIT_MS = 2000;
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Pure: parse Consent Mode v2 entries off a dataLayer
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const SIGNALS = [
|
|
21
|
+
'analytics_storage',
|
|
22
|
+
'ad_storage',
|
|
23
|
+
'ad_user_data',
|
|
24
|
+
'ad_personalization'
|
|
25
|
+
];
|
|
26
|
+
function isSignal(value) {
|
|
27
|
+
return value === 'granted' || value === 'denied';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the mode + partial {@link ConsentState} from a single dataLayer entry
|
|
31
|
+
* IF it is a Consent Mode command (`gtag('consent','default'|'update',{...})`,
|
|
32
|
+
* which lands on the dataLayer as an arguments-like `['consent', mode, params]`).
|
|
33
|
+
* Returns `null` for any non-consent entry. The `mode` matters: a `default` only
|
|
34
|
+
* seeds state, while an `update` is the user's decision (see {@link ConsentGate}).
|
|
35
|
+
*/ function parseConsentEntry(entry) {
|
|
36
|
+
if (entry == null || typeof entry !== 'object') return null;
|
|
37
|
+
// Works for both real arrays and the arguments-objects gtag pushes.
|
|
38
|
+
const indexed = entry;
|
|
39
|
+
if (indexed[0] !== 'consent') return null;
|
|
40
|
+
const mode = indexed[1];
|
|
41
|
+
if (mode !== 'default' && mode !== 'update') return null;
|
|
42
|
+
const params = indexed[2];
|
|
43
|
+
if (params == null || typeof params !== 'object') return null;
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const signal of SIGNALS){
|
|
46
|
+
const v = params[signal];
|
|
47
|
+
if (isSignal(v)) out[signal] = v;
|
|
48
|
+
}
|
|
49
|
+
return Object.keys(out).length > 0 ? {
|
|
50
|
+
mode,
|
|
51
|
+
state: out
|
|
52
|
+
} : null;
|
|
53
|
+
}
|
|
54
|
+
function createConsentGate(initial = DENIED_ALL) {
|
|
55
|
+
let state = {
|
|
56
|
+
...initial
|
|
57
|
+
};
|
|
58
|
+
let resolved = false;
|
|
59
|
+
let buffer = [];
|
|
60
|
+
const listeners = new Set();
|
|
61
|
+
const notify = ()=>{
|
|
62
|
+
for (const l of listeners)l({
|
|
63
|
+
...state
|
|
64
|
+
}, resolved);
|
|
65
|
+
};
|
|
66
|
+
const drain = ()=>{
|
|
67
|
+
if (!resolved) return;
|
|
68
|
+
const pending = buffer;
|
|
69
|
+
buffer = [];
|
|
70
|
+
const granted = state.analytics_storage === 'granted';
|
|
71
|
+
for (const item of pending){
|
|
72
|
+
if (granted) item.effect();
|
|
73
|
+
else item.onDrop?.();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
getState () {
|
|
78
|
+
return {
|
|
79
|
+
...state
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
isGranted (purpose) {
|
|
83
|
+
return state[purpose] === 'granted';
|
|
84
|
+
},
|
|
85
|
+
isResolved () {
|
|
86
|
+
return resolved;
|
|
87
|
+
},
|
|
88
|
+
applyDefault (update) {
|
|
89
|
+
state = {
|
|
90
|
+
...state,
|
|
91
|
+
...update
|
|
92
|
+
};
|
|
93
|
+
notify();
|
|
94
|
+
},
|
|
95
|
+
applyUpdate (update) {
|
|
96
|
+
state = {
|
|
97
|
+
...state,
|
|
98
|
+
...update
|
|
99
|
+
};
|
|
100
|
+
// Only a decision about analytics (or one arriving after we've already
|
|
101
|
+
// resolved) drains. A partial update touching only ad_* keeps buffering.
|
|
102
|
+
if ('analytics_storage' in update || resolved) {
|
|
103
|
+
resolved = true;
|
|
104
|
+
notify();
|
|
105
|
+
drain();
|
|
106
|
+
} else {
|
|
107
|
+
notify();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
resolve () {
|
|
111
|
+
if (resolved) return;
|
|
112
|
+
resolved = true;
|
|
113
|
+
notify();
|
|
114
|
+
drain();
|
|
115
|
+
},
|
|
116
|
+
run (effect, onDrop) {
|
|
117
|
+
if (!resolved) {
|
|
118
|
+
buffer.push({
|
|
119
|
+
effect,
|
|
120
|
+
onDrop
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (state.analytics_storage === 'granted') effect();
|
|
125
|
+
else onDrop?.();
|
|
126
|
+
},
|
|
127
|
+
onChange (listener) {
|
|
128
|
+
listeners.add(listener);
|
|
129
|
+
return ()=>listeners.delete(listener);
|
|
130
|
+
},
|
|
131
|
+
reset () {
|
|
132
|
+
const pending = buffer;
|
|
133
|
+
buffer = [];
|
|
134
|
+
state = {
|
|
135
|
+
...DENIED_ALL
|
|
136
|
+
};
|
|
137
|
+
resolved = true;
|
|
138
|
+
for (const item of pending)item.onDrop?.();
|
|
139
|
+
notify();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function routeConsentEntry(gate, entry) {
|
|
145
|
+
const parsed = parseConsentEntry(entry);
|
|
146
|
+
if (!parsed) return;
|
|
147
|
+
// A `default` only seeds state; an `update` (or explicit setConsent) is the
|
|
148
|
+
// decision that may resolve the gate.
|
|
149
|
+
if (parsed.mode === 'default') gate.applyDefault(parsed.state);
|
|
150
|
+
else gate.applyUpdate(parsed.state);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Zero-config consent: reads Consent Mode v2 commands off `window.dataLayer`
|
|
154
|
+
* (already-present `default`/`update` entries and future pushes) and feeds the
|
|
155
|
+
* gate. Resilient to GTM/gtag.js loading LATER — which reassigns `dataLayer` /
|
|
156
|
+
* its `push` and would discard an in-place patch — via a short re-scan poll over
|
|
157
|
+
* the wait window that re-reads `window.dataLayer` fresh each tick (and re-scans
|
|
158
|
+
* from 0 if the array was replaced). The `push` patch is only a fast path. When
|
|
159
|
+
* running GTM, driving consent explicitly via `setConsent` from the CMP's
|
|
160
|
+
* Consent Mode update callback is the most reliable path.
|
|
161
|
+
*/ function startConsentAutoRead(gate) {
|
|
162
|
+
if (typeof window === 'undefined') return;
|
|
163
|
+
const w = window;
|
|
164
|
+
let scannedArray = null;
|
|
165
|
+
let idx = 0;
|
|
166
|
+
const scan = ()=>{
|
|
167
|
+
const dl = w.dataLayer = w.dataLayer || [];
|
|
168
|
+
if (dl !== scannedArray) {
|
|
169
|
+
// First scan, or the host (GTM) replaced the array — re-read from 0.
|
|
170
|
+
scannedArray = dl;
|
|
171
|
+
idx = 0;
|
|
172
|
+
}
|
|
173
|
+
for(; idx < dl.length; idx++)routeConsentEntry(gate, dl[idx]);
|
|
174
|
+
// Best-effort fast path: observe pushes on the current array (once).
|
|
175
|
+
if (!dl.__cmsConsentObserved) {
|
|
176
|
+
dl.__cmsConsentObserved = true;
|
|
177
|
+
const originalPush = dl.push.bind(dl);
|
|
178
|
+
dl.push = (...args)=>{
|
|
179
|
+
const ret = originalPush(...args);
|
|
180
|
+
try {
|
|
181
|
+
for (const arg of args)routeConsentEntry(gate, arg);
|
|
182
|
+
} catch {
|
|
183
|
+
// never let consent observation break a host dataLayer push
|
|
184
|
+
}
|
|
185
|
+
return ret;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
scan();
|
|
190
|
+
// Poll fallback: survives GTM clobbering the push hook / replacing the array.
|
|
191
|
+
const interval = setInterval(()=>{
|
|
192
|
+
if (gate.isResolved()) {
|
|
193
|
+
clearInterval(interval);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
scan();
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
}, 300);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const PLUGIN_ID = 'consent';
|
|
205
|
+
/** Shallow-equal two consent states across the four Consent Mode v2 signals. */ function sameConsent(a, b) {
|
|
206
|
+
return a.analytics_storage === b.analytics_storage && a.ad_storage === b.ad_storage && a.ad_user_data === b.ad_user_data && a.ad_personalization === b.ad_personalization;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Client plugin that exposes the generic consent gate under its own namespace,
|
|
210
|
+
* decoupled from A/B. Lets any consumer gate side effects or rendering of
|
|
211
|
+
* embedded third-party content (YouTube, Maps, Vimeo) behind Google Consent
|
|
212
|
+
* Mode v2 — render only after the visitor consents.
|
|
213
|
+
*
|
|
214
|
+
* The gate is created once per client (closed over in `getActions`, like
|
|
215
|
+
* `abTest.useLiveResults`), auto-reads Consent Mode commands off the dataLayer,
|
|
216
|
+
* and resolves after a short wait-window. Reached via the client proxy:
|
|
217
|
+
*
|
|
218
|
+
* ```tsx
|
|
219
|
+
* import { consentClient } from '@createcms/core/plugins/consent/client';
|
|
220
|
+
*
|
|
221
|
+
* const client = createCMSClient<typeof cms>({
|
|
222
|
+
* baseURL: '/api/cms',
|
|
223
|
+
* plugins: [consentClient()],
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* // Drive consent from a CMP callback:
|
|
227
|
+
* client.consent.setConsent({ ad_storage: 'granted' });
|
|
228
|
+
*
|
|
229
|
+
* // Gate an embed (component bound to this client's gate):
|
|
230
|
+
* const { ConsentGate } = client.consent;
|
|
231
|
+
* <ConsentGate purpose="ad_storage" fallback={<p>Bitte Cookies akzeptieren.</p>}>
|
|
232
|
+
* <iframe src="https://www.youtube.com/embed/..." />
|
|
233
|
+
* </ConsentGate>
|
|
234
|
+
* ```
|
|
235
|
+
*/ function consentClient() {
|
|
236
|
+
return {
|
|
237
|
+
id: PLUGIN_ID,
|
|
238
|
+
getActions (_$fetch, _$store, _baseURL) {
|
|
239
|
+
const gate = createConsentGate();
|
|
240
|
+
// Zero-config Consent Mode read + a wait-window fallback so a denied
|
|
241
|
+
// default doesn't strand the gate. Render never waits on this; only the
|
|
242
|
+
// gate's buffered side effects do.
|
|
243
|
+
startConsentAutoRead(gate);
|
|
244
|
+
if (typeof window !== 'undefined') {
|
|
245
|
+
setTimeout(()=>gate.resolve(), CONSENT_WAIT_MS);
|
|
246
|
+
}
|
|
247
|
+
/** React hook: subscribe to the gate and re-render on every change. */ function useConsentState() {
|
|
248
|
+
const [snap, setSnap] = useState(()=>({
|
|
249
|
+
state: gate.getState(),
|
|
250
|
+
resolved: gate.isResolved()
|
|
251
|
+
}));
|
|
252
|
+
useEffect(()=>{
|
|
253
|
+
// Re-sync in case a decision landed in the render->effect gap. The
|
|
254
|
+
// functional updater returns `prev` when nothing actually changed so
|
|
255
|
+
// React bails out via Object.is — no wasted render on the common path.
|
|
256
|
+
setSnap((prev)=>{
|
|
257
|
+
const state = gate.getState();
|
|
258
|
+
const resolved = gate.isResolved();
|
|
259
|
+
return prev.resolved === resolved && sameConsent(prev.state, state) ? prev : {
|
|
260
|
+
state,
|
|
261
|
+
resolved
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
return gate.onChange((state, resolved)=>setSnap({
|
|
265
|
+
state,
|
|
266
|
+
resolved
|
|
267
|
+
}));
|
|
268
|
+
}, []);
|
|
269
|
+
return {
|
|
270
|
+
state: snap.state,
|
|
271
|
+
resolved: snap.resolved,
|
|
272
|
+
isGranted: (purpose)=>snap.state[purpose] === 'granted'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Render wrapper bound to this client's gate (default-deny). */ function ConsentGate(props) {
|
|
276
|
+
const { isGranted } = useConsentState();
|
|
277
|
+
return isGranted(props.purpose) ? props.children : props.fallback ?? null;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
consent: {
|
|
281
|
+
/**
|
|
282
|
+
* Tell the CMS about the visitor's consent (Consent Mode v2) — a real
|
|
283
|
+
* decision (treated like a Consent Mode `update`). Optional: the gate
|
|
284
|
+
* also auto-reads Consent Mode commands off the dataLayer; when running
|
|
285
|
+
* GTM, calling this from the CMP's update callback is the most reliable
|
|
286
|
+
* path.
|
|
287
|
+
*/ setConsent (consent) {
|
|
288
|
+
gate.applyUpdate(consent);
|
|
289
|
+
},
|
|
290
|
+
/** Read the current consent state. */ getConsent () {
|
|
291
|
+
return gate.getState();
|
|
292
|
+
},
|
|
293
|
+
/** Whether a given Consent Mode v2 signal is currently granted. */ isGranted (purpose) {
|
|
294
|
+
return gate.isGranted(purpose);
|
|
295
|
+
},
|
|
296
|
+
/** True once a real decision arrived or the wait-window elapsed. */ isResolved () {
|
|
297
|
+
return gate.isResolved();
|
|
298
|
+
},
|
|
299
|
+
/** Subscribe to consent changes. Returns an unsubscribe function. */ onChange (listener) {
|
|
300
|
+
return gate.onChange(listener);
|
|
301
|
+
},
|
|
302
|
+
/** Revoke consent in-session: back to default-deny. */ reset () {
|
|
303
|
+
gate.reset();
|
|
304
|
+
},
|
|
305
|
+
useConsentState,
|
|
306
|
+
ConsentGate
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { consentClient };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
/** Default-deny: nothing is granted until a CMP / Consent Mode signal says so. */ const DENIED_ALL = {
|
|
7
|
+
analytics_storage: 'denied',
|
|
8
|
+
ad_storage: 'denied',
|
|
9
|
+
ad_user_data: 'denied',
|
|
10
|
+
ad_personalization: 'denied'
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* How long (ms) to buffer events before resolving the gate when no consent
|
|
14
|
+
* DECISION has arrived yet — the Consent Mode `wait_for_update` window. Render
|
|
15
|
+
* is NEVER blocked on this; only event emission waits.
|
|
16
|
+
*/ const CONSENT_WAIT_MS = 2000;
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Pure: parse Consent Mode v2 entries off a dataLayer
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const SIGNALS = [
|
|
21
|
+
'analytics_storage',
|
|
22
|
+
'ad_storage',
|
|
23
|
+
'ad_user_data',
|
|
24
|
+
'ad_personalization'
|
|
25
|
+
];
|
|
26
|
+
function isSignal(value) {
|
|
27
|
+
return value === 'granted' || value === 'denied';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the mode + partial {@link ConsentState} from a single dataLayer entry
|
|
31
|
+
* IF it is a Consent Mode command (`gtag('consent','default'|'update',{...})`,
|
|
32
|
+
* which lands on the dataLayer as an arguments-like `['consent', mode, params]`).
|
|
33
|
+
* Returns `null` for any non-consent entry. The `mode` matters: a `default` only
|
|
34
|
+
* seeds state, while an `update` is the user's decision (see {@link ConsentGate}).
|
|
35
|
+
*/ function parseConsentEntry(entry) {
|
|
36
|
+
if (entry == null || typeof entry !== 'object') return null;
|
|
37
|
+
// Works for both real arrays and the arguments-objects gtag pushes.
|
|
38
|
+
const indexed = entry;
|
|
39
|
+
if (indexed[0] !== 'consent') return null;
|
|
40
|
+
const mode = indexed[1];
|
|
41
|
+
if (mode !== 'default' && mode !== 'update') return null;
|
|
42
|
+
const params = indexed[2];
|
|
43
|
+
if (params == null || typeof params !== 'object') return null;
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const signal of SIGNALS){
|
|
46
|
+
const v = params[signal];
|
|
47
|
+
if (isSignal(v)) out[signal] = v;
|
|
48
|
+
}
|
|
49
|
+
return Object.keys(out).length > 0 ? {
|
|
50
|
+
mode,
|
|
51
|
+
state: out
|
|
52
|
+
} : null;
|
|
53
|
+
}
|
|
54
|
+
/** Parses every Consent Mode command on a dataLayer, in order. */ function parseConsentEntries(dataLayer) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const entry of dataLayer){
|
|
57
|
+
const parsed = parseConsentEntry(entry);
|
|
58
|
+
if (parsed) out.push(parsed);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function createConsentGate(initial = DENIED_ALL) {
|
|
63
|
+
let state = {
|
|
64
|
+
...initial
|
|
65
|
+
};
|
|
66
|
+
let resolved = false;
|
|
67
|
+
let buffer = [];
|
|
68
|
+
const listeners = new Set();
|
|
69
|
+
const notify = ()=>{
|
|
70
|
+
for (const l of listeners)l({
|
|
71
|
+
...state
|
|
72
|
+
}, resolved);
|
|
73
|
+
};
|
|
74
|
+
const drain = ()=>{
|
|
75
|
+
if (!resolved) return;
|
|
76
|
+
const pending = buffer;
|
|
77
|
+
buffer = [];
|
|
78
|
+
const granted = state.analytics_storage === 'granted';
|
|
79
|
+
for (const item of pending){
|
|
80
|
+
if (granted) item.effect();
|
|
81
|
+
else item.onDrop?.();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
getState () {
|
|
86
|
+
return {
|
|
87
|
+
...state
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
isGranted (purpose) {
|
|
91
|
+
return state[purpose] === 'granted';
|
|
92
|
+
},
|
|
93
|
+
isResolved () {
|
|
94
|
+
return resolved;
|
|
95
|
+
},
|
|
96
|
+
applyDefault (update) {
|
|
97
|
+
state = {
|
|
98
|
+
...state,
|
|
99
|
+
...update
|
|
100
|
+
};
|
|
101
|
+
notify();
|
|
102
|
+
},
|
|
103
|
+
applyUpdate (update) {
|
|
104
|
+
state = {
|
|
105
|
+
...state,
|
|
106
|
+
...update
|
|
107
|
+
};
|
|
108
|
+
// Only a decision about analytics (or one arriving after we've already
|
|
109
|
+
// resolved) drains. A partial update touching only ad_* keeps buffering.
|
|
110
|
+
if ('analytics_storage' in update || resolved) {
|
|
111
|
+
resolved = true;
|
|
112
|
+
notify();
|
|
113
|
+
drain();
|
|
114
|
+
} else {
|
|
115
|
+
notify();
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
resolve () {
|
|
119
|
+
if (resolved) return;
|
|
120
|
+
resolved = true;
|
|
121
|
+
notify();
|
|
122
|
+
drain();
|
|
123
|
+
},
|
|
124
|
+
run (effect, onDrop) {
|
|
125
|
+
if (!resolved) {
|
|
126
|
+
buffer.push({
|
|
127
|
+
effect,
|
|
128
|
+
onDrop
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (state.analytics_storage === 'granted') effect();
|
|
133
|
+
else onDrop?.();
|
|
134
|
+
},
|
|
135
|
+
onChange (listener) {
|
|
136
|
+
listeners.add(listener);
|
|
137
|
+
return ()=>listeners.delete(listener);
|
|
138
|
+
},
|
|
139
|
+
reset () {
|
|
140
|
+
const pending = buffer;
|
|
141
|
+
buffer = [];
|
|
142
|
+
state = {
|
|
143
|
+
...DENIED_ALL
|
|
144
|
+
};
|
|
145
|
+
resolved = true;
|
|
146
|
+
for (const item of pending)item.onDrop?.();
|
|
147
|
+
notify();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Pure: visitor-key resolution (consent-gated persistence)
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Decides which visitor key to use and whether it may be persisted. Before
|
|
156
|
+
* `analytics_storage` is granted, the key is in-memory only (page lifetime, no
|
|
157
|
+
* device storage). On grant, an existing cookie wins; otherwise the in-memory
|
|
158
|
+
* key is promoted to the cookie so a buffered impression and later events share
|
|
159
|
+
* one identity.
|
|
160
|
+
*/ function resolveVisitorKey(opts) {
|
|
161
|
+
if (opts.granted) {
|
|
162
|
+
if (opts.cookieKey) {
|
|
163
|
+
return {
|
|
164
|
+
key: opts.cookieKey,
|
|
165
|
+
persist: false,
|
|
166
|
+
memKey: opts.cookieKey
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const key = opts.memKey ?? opts.generate();
|
|
170
|
+
return {
|
|
171
|
+
key,
|
|
172
|
+
persist: true,
|
|
173
|
+
memKey: key
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const key = opts.memKey ?? opts.generate();
|
|
177
|
+
return {
|
|
178
|
+
key,
|
|
179
|
+
persist: false,
|
|
180
|
+
memKey: key
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function routeConsentEntry(gate, entry) {
|
|
185
|
+
const parsed = parseConsentEntry(entry);
|
|
186
|
+
if (!parsed) return;
|
|
187
|
+
// A `default` only seeds state; an `update` (or explicit setConsent) is the
|
|
188
|
+
// decision that may resolve the gate.
|
|
189
|
+
if (parsed.mode === 'default') gate.applyDefault(parsed.state);
|
|
190
|
+
else gate.applyUpdate(parsed.state);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Zero-config consent: reads Consent Mode v2 commands off `window.dataLayer`
|
|
194
|
+
* (already-present `default`/`update` entries and future pushes) and feeds the
|
|
195
|
+
* gate. Resilient to GTM/gtag.js loading LATER — which reassigns `dataLayer` /
|
|
196
|
+
* its `push` and would discard an in-place patch — via a short re-scan poll over
|
|
197
|
+
* the wait window that re-reads `window.dataLayer` fresh each tick (and re-scans
|
|
198
|
+
* from 0 if the array was replaced). The `push` patch is only a fast path. When
|
|
199
|
+
* running GTM, driving consent explicitly via `setConsent` from the CMP's
|
|
200
|
+
* Consent Mode update callback is the most reliable path.
|
|
201
|
+
*/ function startConsentAutoRead(gate) {
|
|
202
|
+
if (typeof window === 'undefined') return;
|
|
203
|
+
const w = window;
|
|
204
|
+
let scannedArray = null;
|
|
205
|
+
let idx = 0;
|
|
206
|
+
const scan = ()=>{
|
|
207
|
+
const dl = w.dataLayer = w.dataLayer || [];
|
|
208
|
+
if (dl !== scannedArray) {
|
|
209
|
+
// First scan, or the host (GTM) replaced the array — re-read from 0.
|
|
210
|
+
scannedArray = dl;
|
|
211
|
+
idx = 0;
|
|
212
|
+
}
|
|
213
|
+
for(; idx < dl.length; idx++)routeConsentEntry(gate, dl[idx]);
|
|
214
|
+
// Best-effort fast path: observe pushes on the current array (once).
|
|
215
|
+
if (!dl.__cmsConsentObserved) {
|
|
216
|
+
dl.__cmsConsentObserved = true;
|
|
217
|
+
const originalPush = dl.push.bind(dl);
|
|
218
|
+
dl.push = (...args)=>{
|
|
219
|
+
const ret = originalPush(...args);
|
|
220
|
+
try {
|
|
221
|
+
for (const arg of args)routeConsentEntry(gate, arg);
|
|
222
|
+
} catch {
|
|
223
|
+
// never let consent observation break a host dataLayer push
|
|
224
|
+
}
|
|
225
|
+
return ret;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
scan();
|
|
230
|
+
// Poll fallback: survives GTM clobbering the push hook / replacing the array.
|
|
231
|
+
const interval = setInterval(()=>{
|
|
232
|
+
if (gate.isResolved()) {
|
|
233
|
+
clearInterval(interval);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
scan();
|
|
238
|
+
} catch {
|
|
239
|
+
// ignore
|
|
240
|
+
}
|
|
241
|
+
}, 300);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const PLUGIN_ID = 'consent';
|
|
245
|
+
/**
|
|
246
|
+
* The consent plugin. Owns the generic Google Consent Mode v2 infrastructure
|
|
247
|
+
* (the buffer-then-flush gate, the dataLayer/CMP auto-read, the state model)
|
|
248
|
+
* that any consumer can ride — A/B tracking, analytics sinks, or consent-gated
|
|
249
|
+
* rendering of embedded third-party content.
|
|
250
|
+
*
|
|
251
|
+
* Server-side it is a marker plugin (no schema/endpoints/hooks): consent is a
|
|
252
|
+
* client-side concern today. The client capability (gate + setConsent/getConsent
|
|
253
|
+
* + the <ConsentGate> render wrapper) is exposed via the client entry.
|
|
254
|
+
*/ function consent() {
|
|
255
|
+
return {
|
|
256
|
+
id: PLUGIN_ID
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
exports.CONSENT_WAIT_MS = CONSENT_WAIT_MS;
|
|
261
|
+
exports.DENIED_ALL = DENIED_ALL;
|
|
262
|
+
exports.consent = consent;
|
|
263
|
+
exports.createConsentGate = createConsentGate;
|
|
264
|
+
exports.parseConsentEntries = parseConsentEntries;
|
|
265
|
+
exports.parseConsentEntry = parseConsentEntry;
|
|
266
|
+
exports.resolveVisitorKey = resolveVisitorKey;
|
|
267
|
+
exports.startConsentAutoRead = startConsentAutoRead;
|