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