@replohq/sdk 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_vendor/replo-utils/lib/url.d.ts +5 -0
- package/_vendor/schemas/generated/consent.d.ts +18 -1
- package/_vendor/schemas/routing/locale.d.ts +45 -0
- package/_vendor/schemas/routing/locale.mjs +95 -0
- package/_vendor/schemas/routing/localeNegotiation.d.ts +87 -0
- package/_vendor/schemas/routing/localeNegotiation.mjs +141 -0
- package/_vendor/schemas/routing/rules.mjs +1 -1
- package/analytics/analytics-provider.js +2 -0
- package/analytics/analytics-provider.js.map +2 -2
- package/analytics/get-analytics-sinks.d.ts +1 -0
- package/analytics/get-analytics-sinks.js +4 -1
- package/analytics/get-analytics-sinks.js.map +2 -2
- package/analytics/replo-pixel-script.d.ts +4 -0
- package/analytics/replo-pixel-script.js +10 -2
- package/analytics/replo-pixel-script.js.map +2 -2
- package/analytics/utils/analytics-utils.js +5 -6
- package/analytics/utils/analytics-utils.js.map +2 -2
- package/consent/consent-platform.d.ts +52 -0
- package/consent/consent-platform.js +223 -0
- package/consent/consent-platform.js.map +7 -0
- package/consent/consent-store.d.ts +4 -3
- package/consent/consent-store.js +28 -10
- package/consent/consent-store.js.map +2 -2
- package/consent/inject-script-descriptors.d.ts +3 -1
- package/consent/inject-script-descriptors.js +17 -1
- package/consent/inject-script-descriptors.js.map +2 -2
- package/consent/replo-scripts.d.ts +9 -2
- package/consent/replo-scripts.js +66 -7
- package/consent/replo-scripts.js.map +2 -2
- package/consent/script-snippets.d.ts +1 -1
- package/consent/script-snippets.js.map +2 -2
- package/consent/types.d.ts +13 -2
- package/consent/window-api.d.ts +4 -1
- package/consent/window-api.js +3 -0
- package/consent/window-api.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/package.json +14 -4
- package/routing/evaluate.d.ts +3 -1
- package/routing/evaluate.js +150 -6
- package/routing/evaluate.js.map +3 -3
- package/routing/geo.d.ts +9 -0
- package/routing/geo.js +17 -0
- package/routing/geo.js.map +7 -0
- package/routing/locale.d.ts +13 -0
- package/routing/locale.js +13 -0
- package/routing/locale.js.map +7 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../analytics/utils/analytics-utils.ts"],
|
|
4
|
-
"sourcesContent": ["// oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\ntype QueuedEvent = { event: string; params: Record<string, any> };\n\nexport function createEventQueue(\n checkPixelReady: () => boolean,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n sendEvent: (event: string, params: Record<string, any>) => void,\n pixelName: string,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (event: string, params?: Record<string, any>) => void {\n const queue: QueuedEvent[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n if (!checkPixelReady()) {\n return;\n }\n console.debug(\n `[analytics] flushing ${queue.length} queued events for ${pixelName}`,\n );\n try {\n queue.forEach(({ event, params }) => sendEvent(event, params));\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n console.debug(
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["// oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\ntype QueuedEvent = { event: string; params: Record<string, any> };\n\nconst QUEUE_TIMEOUT_MS = 30_000;\n\nexport function createEventQueue(\n checkPixelReady: () => boolean,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n sendEvent: (event: string, params: Record<string, any>) => void,\n pixelName: string,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (event: string, params?: Record<string, any>) => void {\n const queue: QueuedEvent[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n if (!checkPixelReady()) {\n return;\n }\n console.debug(\n `[analytics] flushing ${queue.length} queued events for ${pixelName}`,\n );\n try {\n queue.forEach(({ event, params }) => sendEvent(event, params));\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n console.debug(`Analytics queue timed out for ${pixelName}`);\n cleanup();\n }, QUEUE_TIMEOUT_MS);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (event: string, params: Record<string, any> = {}) => {\n if (permanentlyDisabled) {\n console.debug(\n `[analytics] ${pixelName} queue is disabled, dropping event`,\n event,\n );\n return;\n }\n\n if (checkPixelReady()) {\n sendEvent(event, params);\n } else {\n queue.push({ event, params });\n scheduleFlush();\n }\n };\n}\n\ntype MethodQueueOptions = {\n queueName?: string;\n onTimeout?: () => void;\n};\n\nexport function createMethodQueue<\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n InstanceType extends Record<string, (...args: any[]) => any>,\n>(\n checkInstanceReady: () => InstanceType | null,\n options?: MethodQueueOptions,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (method: keyof InstanceType, ...args: any[]) => void {\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n const queue: { method: keyof InstanceType; args: any[] }[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n const queueName = options?.queueName ?? \"method queue\";\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n const instance = checkInstanceReady();\n if (!instance) {\n return;\n }\n try {\n queue.forEach(({ method, args }) => {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n });\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n if (typeof window !== \"undefined\") {\n console.debug(\n `[analytics] ${queueName} timed out and has been disabled`,\n );\n }\n options?.onTimeout?.();\n cleanup();\n }, QUEUE_TIMEOUT_MS);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (method: keyof InstanceType, ...args: any[]) => {\n if (permanentlyDisabled) {\n return;\n }\n\n const instance = checkInstanceReady();\n if (instance) {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n } else {\n queue.push({ method, args });\n scheduleFlush();\n }\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ConsentCategories } from "./cookie";
|
|
2
|
+
import type { ConsentCategory, ReploScriptEntry, ScriptTagDescriptor } from "./types";
|
|
3
|
+
export interface ConsentPlatformConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
loader: ScriptTagDescriptor[];
|
|
6
|
+
blockingAttribute: {
|
|
7
|
+
name: string;
|
|
8
|
+
values: Partial<Record<ConsentCategory, string>>;
|
|
9
|
+
};
|
|
10
|
+
/** Cookiebot preset only: enables consent read-back and runScripts re-scan. */
|
|
11
|
+
isCookiebot: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Whether this entry connects a consent platform (vs a tracker it would gate). */
|
|
14
|
+
export declare function isConsentPlatformEntry(entry: ReploScriptEntry): boolean;
|
|
15
|
+
/** The connected platform's config, or null when Replo's banner owns consent. */
|
|
16
|
+
export declare function findConsentPlatform(scripts: ReploScriptEntry[]): ConsentPlatformConfig | null;
|
|
17
|
+
/** Blocks a gated script; `text/plain` is unconditional so an unmapped category fails closed. */
|
|
18
|
+
export declare function getBlockingAttributes(config: ConsentPlatformConfig, requiredConsent: ConsentCategory[]): Record<string, string>;
|
|
19
|
+
/** The bits of Cookiebot's `window.Cookiebot` global we read. */
|
|
20
|
+
interface CookiebotGlobal {
|
|
21
|
+
consent?: Partial<Record<CookiebotCategory, boolean>>;
|
|
22
|
+
hasResponse?: boolean;
|
|
23
|
+
runScripts?: () => void;
|
|
24
|
+
}
|
|
25
|
+
type CookiebotCategory = "necessary" | "preferences" | "statistics" | "marketing";
|
|
26
|
+
declare global {
|
|
27
|
+
interface Window {
|
|
28
|
+
Cookiebot?: CookiebotGlobal;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
type OptionalConsentCategory = Exclude<ConsentCategory, "necessary">;
|
|
32
|
+
export declare const OPTIONAL_CONSENT_CATEGORIES: readonly ["analytics", "marketing", "preferences", "sale_of_data"];
|
|
33
|
+
/** The delegated platform, or null when Replo's own banner owns consent. */
|
|
34
|
+
export declare function findDelegatedPlatform(): ConsentPlatformConfig | null;
|
|
35
|
+
/** The platform's answer as Replo consent. */
|
|
36
|
+
export declare function getDelegatedConsent(): {
|
|
37
|
+
categories: ConsentCategories;
|
|
38
|
+
decidedAt: number | null;
|
|
39
|
+
};
|
|
40
|
+
/** Inline marker a generic CMP executes when it releases one consent category. */
|
|
41
|
+
export declare function buildGrantMarker(category: OptionalConsentCategory): ScriptTagDescriptor;
|
|
42
|
+
/** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */
|
|
43
|
+
export declare function enableConsentDelegation(config: ConsentPlatformConfig): void;
|
|
44
|
+
/** Cookiebot-only re-scan; other platforms rely on the loader injecting after every tag. */
|
|
45
|
+
export declare function activateBlockedScripts(): void;
|
|
46
|
+
/** Subscribe to delegated-consent changes. Returns an unsubscribe function. */
|
|
47
|
+
export declare function subscribeToConsentDelegation(listener: () => void): () => void;
|
|
48
|
+
/** Replaces navigation in tests so withdrawal behavior can be asserted. */
|
|
49
|
+
export declare function setConsentPlatformReloadForTests(reload: () => void): void;
|
|
50
|
+
/** Test-only reset; leaves subscriptions alone so the store's own stays attached. */
|
|
51
|
+
export declare function resetConsentPlatformForTests(): void;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DENIED_CATEGORIES } from "./cookie";
|
|
3
|
+
const COOKIEBOT_CATEGORY_BY_CONSENT = {
|
|
4
|
+
analytics: "statistics",
|
|
5
|
+
marketing: "marketing",
|
|
6
|
+
preferences: "preferences",
|
|
7
|
+
sale_of_data: "marketing"
|
|
8
|
+
};
|
|
9
|
+
function cookiebotPreset(identifier) {
|
|
10
|
+
return {
|
|
11
|
+
id: "cookiebot",
|
|
12
|
+
loader: [
|
|
13
|
+
{
|
|
14
|
+
kind: "external",
|
|
15
|
+
src: "https://consent.cookiebot.com/uc.js",
|
|
16
|
+
attributes: {
|
|
17
|
+
id: "Cookiebot",
|
|
18
|
+
"data-cbid": identifier
|
|
19
|
+
// No data-blockingmode: uc.js only ever checks for "auto"; manual is
|
|
20
|
+
// its coded default, so any other value (or absence) is identical.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
blockingAttribute: {
|
|
25
|
+
name: "data-cookieconsent",
|
|
26
|
+
values: COOKIEBOT_CATEGORY_BY_CONSENT
|
|
27
|
+
},
|
|
28
|
+
isCookiebot: true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function isConsentPlatformEntry(entry) {
|
|
32
|
+
return entry.type === "Cookiebot" || entry.type === "consentPlatform";
|
|
33
|
+
}
|
|
34
|
+
function findConsentPlatform(scripts) {
|
|
35
|
+
for (const entry of scripts) {
|
|
36
|
+
if (entry.type === "Cookiebot") {
|
|
37
|
+
return cookiebotPreset(entry.identifier);
|
|
38
|
+
}
|
|
39
|
+
if (entry.type === "consentPlatform") {
|
|
40
|
+
return {
|
|
41
|
+
id: entry.id,
|
|
42
|
+
loader: [
|
|
43
|
+
{ kind: "external", src: entry.src, attributes: entry.attributes }
|
|
44
|
+
],
|
|
45
|
+
blockingAttribute: entry.blockingAttribute,
|
|
46
|
+
isCookiebot: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function getBlockingAttributes(config, requiredConsent) {
|
|
53
|
+
const gated = requiredConsent.filter((category) => category !== "necessary");
|
|
54
|
+
if (gated.length === 0) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
const tokens = [
|
|
58
|
+
...new Set(
|
|
59
|
+
gated.flatMap(
|
|
60
|
+
(category) => config.blockingAttribute.values[category] ?? []
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
];
|
|
64
|
+
return {
|
|
65
|
+
type: "text/plain",
|
|
66
|
+
...tokens.length > 0 ? { [config.blockingAttribute.name]: tokens.join(",") } : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const COOKIEBOT_EVENTS = [
|
|
70
|
+
"CookiebotOnConsentReady",
|
|
71
|
+
"CookiebotOnAccept",
|
|
72
|
+
"CookiebotOnDecline"
|
|
73
|
+
];
|
|
74
|
+
const OPTIONAL_CONSENT_CATEGORIES = [
|
|
75
|
+
"analytics",
|
|
76
|
+
"marketing",
|
|
77
|
+
"preferences",
|
|
78
|
+
"sale_of_data"
|
|
79
|
+
];
|
|
80
|
+
const genericConsentCategorySchema = z.enum(OPTIONAL_CONSENT_CATEGORIES);
|
|
81
|
+
const GENERIC_CONSENT_GRANTED_EVENT = "reploConsentCategoryGranted";
|
|
82
|
+
let delegatedPlatform = null;
|
|
83
|
+
let listeningToCookiebot = false;
|
|
84
|
+
let listeningToGenericPlatform = false;
|
|
85
|
+
let categories = DENIED_CATEGORIES;
|
|
86
|
+
let decidedAt = null;
|
|
87
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
88
|
+
const reloadCurrentPage = () => window.location.reload();
|
|
89
|
+
let reloadPage = reloadCurrentPage;
|
|
90
|
+
function findDelegatedPlatform() {
|
|
91
|
+
return delegatedPlatform;
|
|
92
|
+
}
|
|
93
|
+
function getDelegatedConsent() {
|
|
94
|
+
return { categories, decidedAt };
|
|
95
|
+
}
|
|
96
|
+
function readCookiebotConsent() {
|
|
97
|
+
const cookiebot = typeof window === "undefined" ? void 0 : window.Cookiebot;
|
|
98
|
+
if (!cookiebot?.consent || cookiebot.hasResponse !== true) {
|
|
99
|
+
return { hasChanged: false, hasDowngrade: false };
|
|
100
|
+
}
|
|
101
|
+
const consent = cookiebot.consent;
|
|
102
|
+
const next = {
|
|
103
|
+
necessary: true,
|
|
104
|
+
analytics: consent.statistics === true,
|
|
105
|
+
marketing: consent.marketing === true,
|
|
106
|
+
preferences: consent.preferences === true,
|
|
107
|
+
sale_of_data: consent.marketing === true
|
|
108
|
+
};
|
|
109
|
+
const unchanged = next.analytics === categories.analytics && next.marketing === categories.marketing && next.preferences === categories.preferences && next.sale_of_data === categories.sale_of_data;
|
|
110
|
+
if (decidedAt !== null && unchanged) {
|
|
111
|
+
return { hasChanged: false, hasDowngrade: false };
|
|
112
|
+
}
|
|
113
|
+
const hasDowngrade = decidedAt !== null && OPTIONAL_CONSENT_CATEGORIES.some((category) => {
|
|
114
|
+
return categories[category] && !next[category];
|
|
115
|
+
});
|
|
116
|
+
categories = next;
|
|
117
|
+
decidedAt = Date.now();
|
|
118
|
+
return { hasChanged: true, hasDowngrade };
|
|
119
|
+
}
|
|
120
|
+
function notifyConsentListeners() {
|
|
121
|
+
for (const listener of listeners) {
|
|
122
|
+
listener();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function handleCookiebotEvent() {
|
|
126
|
+
const result = readCookiebotConsent();
|
|
127
|
+
if (!result.hasChanged) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
notifyConsentListeners();
|
|
131
|
+
if (result.hasDowngrade) {
|
|
132
|
+
reloadPage();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function handleGenericConsentActivation(event) {
|
|
136
|
+
if (!(event instanceof CustomEvent)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const result = genericConsentCategorySchema.safeParse(event.detail);
|
|
140
|
+
if (!result.success || categories[result.data]) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
categories = { ...categories, [result.data]: true };
|
|
144
|
+
decidedAt = Date.now();
|
|
145
|
+
notifyConsentListeners();
|
|
146
|
+
}
|
|
147
|
+
function buildGrantMarker(category) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "inline",
|
|
150
|
+
body: `document.dispatchEvent(new CustomEvent(${JSON.stringify(GENERIC_CONSENT_GRANTED_EVENT)}, { detail: ${JSON.stringify(category)} }));`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function enableConsentDelegation(config) {
|
|
154
|
+
if (typeof window === "undefined" || delegatedPlatform) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
delegatedPlatform = config;
|
|
158
|
+
if (config.isCookiebot && !listeningToCookiebot) {
|
|
159
|
+
listeningToCookiebot = true;
|
|
160
|
+
for (const event of COOKIEBOT_EVENTS) {
|
|
161
|
+
window.addEventListener(event, handleCookiebotEvent);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!config.isCookiebot && !listeningToGenericPlatform) {
|
|
165
|
+
listeningToGenericPlatform = true;
|
|
166
|
+
document.addEventListener(
|
|
167
|
+
GENERIC_CONSENT_GRANTED_EVENT,
|
|
168
|
+
handleGenericConsentActivation
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (config.isCookiebot) {
|
|
172
|
+
readCookiebotConsent();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function activateBlockedScripts() {
|
|
176
|
+
if (!delegatedPlatform?.isCookiebot) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
window.Cookiebot?.runScripts?.();
|
|
180
|
+
}
|
|
181
|
+
function subscribeToConsentDelegation(listener) {
|
|
182
|
+
listeners.add(listener);
|
|
183
|
+
return () => {
|
|
184
|
+
listeners.delete(listener);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function setConsentPlatformReloadForTests(reload) {
|
|
188
|
+
reloadPage = reload;
|
|
189
|
+
}
|
|
190
|
+
function resetConsentPlatformForTests() {
|
|
191
|
+
if (typeof window !== "undefined" && listeningToCookiebot) {
|
|
192
|
+
for (const event of COOKIEBOT_EVENTS) {
|
|
193
|
+
window.removeEventListener(event, handleCookiebotEvent);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof document !== "undefined" && listeningToGenericPlatform) {
|
|
197
|
+
document.removeEventListener(
|
|
198
|
+
GENERIC_CONSENT_GRANTED_EVENT,
|
|
199
|
+
handleGenericConsentActivation
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
delegatedPlatform = null;
|
|
203
|
+
listeningToCookiebot = false;
|
|
204
|
+
listeningToGenericPlatform = false;
|
|
205
|
+
categories = DENIED_CATEGORIES;
|
|
206
|
+
decidedAt = null;
|
|
207
|
+
reloadPage = reloadCurrentPage;
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
OPTIONAL_CONSENT_CATEGORIES,
|
|
211
|
+
activateBlockedScripts,
|
|
212
|
+
buildGrantMarker,
|
|
213
|
+
enableConsentDelegation,
|
|
214
|
+
findConsentPlatform,
|
|
215
|
+
findDelegatedPlatform,
|
|
216
|
+
getBlockingAttributes,
|
|
217
|
+
getDelegatedConsent,
|
|
218
|
+
isConsentPlatformEntry,
|
|
219
|
+
resetConsentPlatformForTests,
|
|
220
|
+
setConsentPlatformReloadForTests,
|
|
221
|
+
subscribeToConsentDelegation
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=consent-platform.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../consent/consent-platform.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ConsentCategories } from \"./cookie\";\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"./types\";\n\nimport { z } from \"zod\";\n\nimport { DENIED_CATEGORIES } from \"./cookie\";\n\n// A connected consent platform owns consent instead of Replo's banner: managed\n// tags are blocked until it activates them, and Replo stores no consent itself.\n\nexport interface ConsentPlatformConfig {\n id: string;\n loader: ScriptTagDescriptor[];\n blockingAttribute: {\n name: string;\n values: Partial<Record<ConsentCategory, string>>;\n };\n /** Cookiebot preset only: enables consent read-back and runScripts re-scan. */\n isCookiebot: boolean;\n}\n\n/** Cookiebot's fixed vocabulary. It has no sale-of-data category, so marketing covers it; `necessary` is never blocked. The `satisfies` makes a future ConsentCategory a compile error instead of a script that silently runs before consent. */\nconst COOKIEBOT_CATEGORY_BY_CONSENT: Partial<Record<ConsentCategory, string>> =\n {\n analytics: \"statistics\",\n marketing: \"marketing\",\n preferences: \"preferences\",\n sale_of_data: \"marketing\",\n } satisfies Record<Exclude<ConsentCategory, \"necessary\">, string>;\n\nfunction cookiebotPreset(identifier: string): ConsentPlatformConfig {\n return {\n id: \"cookiebot\",\n loader: [\n {\n kind: \"external\",\n src: \"https://consent.cookiebot.com/uc.js\",\n attributes: {\n id: \"Cookiebot\",\n \"data-cbid\": identifier,\n // No data-blockingmode: uc.js only ever checks for \"auto\"; manual is\n // its coded default, so any other value (or absence) is identical.\n },\n },\n ],\n blockingAttribute: {\n name: \"data-cookieconsent\",\n values: COOKIEBOT_CATEGORY_BY_CONSENT,\n },\n isCookiebot: true,\n };\n}\n\n/** Whether this entry connects a consent platform (vs a tracker it would gate). */\nexport function isConsentPlatformEntry(entry: ReploScriptEntry): boolean {\n return entry.type === \"Cookiebot\" || entry.type === \"consentPlatform\";\n}\n\n/** The connected platform's config, or null when Replo's banner owns consent. */\nexport function findConsentPlatform(\n scripts: ReploScriptEntry[],\n): ConsentPlatformConfig | null {\n for (const entry of scripts) {\n if (entry.type === \"Cookiebot\") {\n return cookiebotPreset(entry.identifier);\n }\n if (entry.type === \"consentPlatform\") {\n return {\n id: entry.id,\n loader: [\n { kind: \"external\", src: entry.src, attributes: entry.attributes },\n ],\n blockingAttribute: entry.blockingAttribute,\n isCookiebot: false,\n };\n }\n }\n return null;\n}\n\n/** Blocks a gated script; `text/plain` is unconditional so an unmapped category fails closed. */\nexport function getBlockingAttributes(\n config: ConsentPlatformConfig,\n requiredConsent: ConsentCategory[],\n): Record<string, string> {\n const gated = requiredConsent.filter((category) => category !== \"necessary\");\n if (gated.length === 0) {\n return {};\n }\n const tokens = [\n ...new Set(\n gated.flatMap(\n (category) => config.blockingAttribute.values[category] ?? [],\n ),\n ),\n ];\n return {\n type: \"text/plain\",\n ...(tokens.length > 0\n ? { [config.blockingAttribute.name]: tokens.join(\",\") }\n : {}),\n };\n}\n\n/** The bits of Cookiebot's `window.Cookiebot` global we read. */\ninterface CookiebotGlobal {\n consent?: Partial<Record<CookiebotCategory, boolean>>;\n hasResponse?: boolean;\n runScripts?: () => void;\n}\n\ntype CookiebotCategory =\n | \"necessary\"\n | \"preferences\"\n | \"statistics\"\n | \"marketing\";\n\ndeclare global {\n interface Window {\n Cookiebot?: CookiebotGlobal;\n }\n}\n\n/** `CookiebotOnConsentReady` also fires on load for an already-stored answer, catching returning visitors. */\nconst COOKIEBOT_EVENTS = [\n \"CookiebotOnConsentReady\",\n \"CookiebotOnAccept\",\n \"CookiebotOnDecline\",\n] as const;\n\ntype OptionalConsentCategory = Exclude<ConsentCategory, \"necessary\">;\n\nexport const OPTIONAL_CONSENT_CATEGORIES = [\n \"analytics\",\n \"marketing\",\n \"preferences\",\n \"sale_of_data\",\n] as const satisfies readonly OptionalConsentCategory[];\n\nconst genericConsentCategorySchema = z.enum(OPTIONAL_CONSENT_CATEGORIES);\nconst GENERIC_CONSENT_GRANTED_EVENT = \"reploConsentCategoryGranted\";\n\nlet delegatedPlatform: ConsentPlatformConfig | null = null;\nlet listeningToCookiebot = false;\nlet listeningToGenericPlatform = false;\nlet categories: ConsentCategories = DENIED_CATEGORIES;\nlet decidedAt: number | null = null;\nconst listeners = new Set<() => void>();\nconst reloadCurrentPage = () => window.location.reload();\nlet reloadPage = reloadCurrentPage;\n\n/** The delegated platform, or null when Replo's own banner owns consent. */\nexport function findDelegatedPlatform(): ConsentPlatformConfig | null {\n return delegatedPlatform;\n}\n\n/** The platform's answer as Replo consent. */\nexport function getDelegatedConsent(): {\n categories: ConsentCategories;\n decidedAt: number | null;\n} {\n return { categories, decidedAt };\n}\n\ninterface CookiebotConsentReadResult {\n hasChanged: boolean;\n hasDowngrade: boolean;\n}\n\n/** Reads Cookiebot's answer into module state. */\nfunction readCookiebotConsent(): CookiebotConsentReadResult {\n const cookiebot =\n typeof window === \"undefined\" ? undefined : window.Cookiebot;\n if (!cookiebot?.consent || cookiebot.hasResponse !== true) {\n return { hasChanged: false, hasDowngrade: false };\n }\n const consent = cookiebot.consent;\n const next: ConsentCategories = {\n necessary: true,\n analytics: consent.statistics === true,\n marketing: consent.marketing === true,\n preferences: consent.preferences === true,\n sale_of_data: consent.marketing === true,\n };\n // Consumers memoize on the categories object, so only replace it when the answer moved.\n const unchanged =\n next.analytics === categories.analytics &&\n next.marketing === categories.marketing &&\n next.preferences === categories.preferences &&\n next.sale_of_data === categories.sale_of_data;\n if (decidedAt !== null && unchanged) {\n return { hasChanged: false, hasDowngrade: false };\n }\n const hasDowngrade =\n decidedAt !== null &&\n OPTIONAL_CONSENT_CATEGORIES.some((category) => {\n return categories[category] && !next[category];\n });\n categories = next;\n decidedAt = Date.now();\n return { hasChanged: true, hasDowngrade };\n}\n\nfunction notifyConsentListeners() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction handleCookiebotEvent() {\n const result = readCookiebotConsent();\n if (!result.hasChanged) {\n return;\n }\n notifyConsentListeners();\n if (result.hasDowngrade) {\n reloadPage();\n }\n}\n\nfunction handleGenericConsentActivation(event: Event) {\n if (!(event instanceof CustomEvent)) {\n return;\n }\n const result = genericConsentCategorySchema.safeParse(event.detail);\n if (!result.success || categories[result.data]) {\n return;\n }\n categories = { ...categories, [result.data]: true };\n decidedAt = Date.now();\n notifyConsentListeners();\n}\n\n/** Inline marker a generic CMP executes when it releases one consent category. */\nexport function buildGrantMarker(\n category: OptionalConsentCategory,\n): ScriptTagDescriptor {\n return {\n kind: \"inline\",\n body: `document.dispatchEvent(new CustomEvent(${JSON.stringify(GENERIC_CONSENT_GRANTED_EVENT)}, { detail: ${JSON.stringify(category)} }));`,\n };\n}\n\n/** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */\nexport function enableConsentDelegation(config: ConsentPlatformConfig) {\n // Client-only: script gating is a DOM concern, and latching module state\n // during SSR would leak the config across requests in a Worker isolate.\n if (typeof window === \"undefined\" || delegatedPlatform) {\n return;\n }\n delegatedPlatform = config;\n if (config.isCookiebot && !listeningToCookiebot) {\n listeningToCookiebot = true;\n for (const event of COOKIEBOT_EVENTS) {\n window.addEventListener(event, handleCookiebotEvent);\n }\n }\n if (!config.isCookiebot && !listeningToGenericPlatform) {\n listeningToGenericPlatform = true;\n document.addEventListener(\n GENERIC_CONSENT_GRANTED_EVENT,\n handleGenericConsentActivation,\n );\n }\n if (config.isCookiebot) {\n // No notify: this runs during render, where notifying re-renders other components.\n readCookiebotConsent();\n }\n}\n\n/** Cookiebot-only re-scan; other platforms rely on the loader injecting after every tag. */\nexport function activateBlockedScripts() {\n if (!delegatedPlatform?.isCookiebot) {\n return;\n }\n window.Cookiebot?.runScripts?.();\n}\n\n/** Subscribe to delegated-consent changes. Returns an unsubscribe function. */\nexport function subscribeToConsentDelegation(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Replaces navigation in tests so withdrawal behavior can be asserted. */\nexport function setConsentPlatformReloadForTests(reload: () => void) {\n reloadPage = reload;\n}\n\n/** Test-only reset; leaves subscriptions alone so the store's own stays attached. */\nexport function resetConsentPlatformForTests() {\n if (typeof window !== \"undefined\" && listeningToCookiebot) {\n for (const event of COOKIEBOT_EVENTS) {\n window.removeEventListener(event, handleCookiebotEvent);\n }\n }\n if (typeof document !== \"undefined\" && listeningToGenericPlatform) {\n document.removeEventListener(\n GENERIC_CONSENT_GRANTED_EVENT,\n handleGenericConsentActivation,\n );\n }\n delegatedPlatform = null;\n listeningToCookiebot = false;\n listeningToGenericPlatform = false;\n categories = DENIED_CATEGORIES;\n decidedAt = null;\n reloadPage = reloadCurrentPage;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -13,6 +13,8 @@ import type { ConsentCategories } from "./cookie";
|
|
|
13
13
|
* attribute on `<html>`.
|
|
14
14
|
*/
|
|
15
15
|
export interface ConsentState {
|
|
16
|
+
/** Which platform owns consent. Absent means Replo's native banner; "external" is a generic `consentPlatform` entry. */
|
|
17
|
+
cmp?: "replo" | "cookiebot" | "external";
|
|
16
18
|
mode: ConsentMode;
|
|
17
19
|
categories: ConsentCategories;
|
|
18
20
|
/** Epoch ms when the visitor last made a choice, or null if undecided. */
|
|
@@ -59,9 +61,8 @@ export interface UseConsentResult extends ConsentState {
|
|
|
59
61
|
}
|
|
60
62
|
export declare function useConsent(options?: UseConsentOptions): UseConsentResult;
|
|
61
63
|
/**
|
|
62
|
-
* Whether a script requiring `requiredConsent`
|
|
63
|
-
*
|
|
64
|
-
* granted. An empty requirement list is always allowed.
|
|
64
|
+
* Whether a script requiring `requiredConsent` may load now. `mode === "off"`
|
|
65
|
+
* allows everything only under Replo's banner; a delegated CMP answers instead.
|
|
65
66
|
*/
|
|
66
67
|
export declare function isConsentAllowed({ state, requiredConsent, }: {
|
|
67
68
|
state: ConsentState;
|
package/consent/consent-store.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
3
|
import { deriveConsentAction } from "./consent-action";
|
|
4
4
|
import { recordConsentAction } from "./consent-actions";
|
|
5
|
+
import {
|
|
6
|
+
findDelegatedPlatform,
|
|
7
|
+
getDelegatedConsent,
|
|
8
|
+
OPTIONAL_CONSENT_CATEGORIES,
|
|
9
|
+
subscribeToConsentDelegation
|
|
10
|
+
} from "./consent-platform";
|
|
5
11
|
import {
|
|
6
12
|
CONSENT_COOKIE_MAX_AGE_SECONDS,
|
|
7
13
|
CONSENT_COOKIE_NAME,
|
|
@@ -10,13 +16,8 @@ import {
|
|
|
10
16
|
parseConsentCookie,
|
|
11
17
|
serializeConsentCookieValue
|
|
12
18
|
} from "./cookie";
|
|
13
|
-
const TOGGLEABLE_CATEGORIES = [
|
|
14
|
-
"analytics",
|
|
15
|
-
"marketing",
|
|
16
|
-
"preferences",
|
|
17
|
-
"sale_of_data"
|
|
18
|
-
];
|
|
19
19
|
const SERVER_SNAPSHOT = Object.freeze({
|
|
20
|
+
cmp: "replo",
|
|
20
21
|
mode: "off",
|
|
21
22
|
categories: DENIED_CATEGORIES,
|
|
22
23
|
decidedAt: null
|
|
@@ -39,9 +40,18 @@ function computeSnapshot() {
|
|
|
39
40
|
return SERVER_SNAPSHOT;
|
|
40
41
|
}
|
|
41
42
|
const mode = resolveMode();
|
|
43
|
+
const platform = findDelegatedPlatform();
|
|
44
|
+
if (platform) {
|
|
45
|
+
return {
|
|
46
|
+
cmp: platform.isCookiebot ? "cookiebot" : "external",
|
|
47
|
+
mode,
|
|
48
|
+
...getDelegatedConsent()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
42
51
|
const fromCookie = parseConsentCookie(document.cookie);
|
|
43
52
|
if (fromCookie) {
|
|
44
53
|
return {
|
|
54
|
+
cmp: "replo",
|
|
45
55
|
mode,
|
|
46
56
|
categories: {
|
|
47
57
|
necessary: true,
|
|
@@ -54,7 +64,7 @@ function computeSnapshot() {
|
|
|
54
64
|
consentId: fromCookie.consentId
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
|
-
return { mode, categories: DENIED_CATEGORIES, decidedAt: null };
|
|
67
|
+
return { cmp: "replo", mode, categories: DENIED_CATEGORIES, decidedAt: null };
|
|
58
68
|
}
|
|
59
69
|
function writeCookie(categories, decidedAt, consentId) {
|
|
60
70
|
if (typeof document === "undefined") {
|
|
@@ -77,6 +87,9 @@ function commit({
|
|
|
77
87
|
policyVersion = DEFAULT_CONSENT_POLICY_VERSION
|
|
78
88
|
}) {
|
|
79
89
|
const previous = consentStore.getSnapshot();
|
|
90
|
+
if (previous.cmp === "cookiebot" || previous.cmp === "external") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
80
93
|
const decidedAt = Date.now();
|
|
81
94
|
const consentId = previous.consentId ?? crypto.randomUUID();
|
|
82
95
|
const resultingCategories = {
|
|
@@ -85,13 +98,14 @@ function commit({
|
|
|
85
98
|
};
|
|
86
99
|
writeCookie(categories, decidedAt, consentId);
|
|
87
100
|
snapshot = {
|
|
101
|
+
cmp: previous.cmp,
|
|
88
102
|
mode: previous.mode,
|
|
89
103
|
categories: resultingCategories,
|
|
90
104
|
decidedAt,
|
|
91
105
|
consentId
|
|
92
106
|
};
|
|
93
107
|
notify();
|
|
94
|
-
const wasDowngrade = previous.decidedAt !== null &&
|
|
108
|
+
const wasDowngrade = previous.decidedAt !== null && OPTIONAL_CONSENT_CATEGORIES.some((category) => {
|
|
95
109
|
return previous.categories[category] && !categories[category];
|
|
96
110
|
});
|
|
97
111
|
const reloadIfDowngraded = () => {
|
|
@@ -114,7 +128,7 @@ function commit({
|
|
|
114
128
|
}
|
|
115
129
|
const consentStore = {
|
|
116
130
|
getSnapshot() {
|
|
117
|
-
if (!snapshot) {
|
|
131
|
+
if (!snapshot || findDelegatedPlatform() !== null && (snapshot.cmp ?? "replo") === "replo") {
|
|
118
132
|
snapshot = computeSnapshot();
|
|
119
133
|
}
|
|
120
134
|
return snapshot;
|
|
@@ -178,7 +192,7 @@ function isConsentAllowed({
|
|
|
178
192
|
state,
|
|
179
193
|
requiredConsent
|
|
180
194
|
}) {
|
|
181
|
-
if (state.mode === "off") {
|
|
195
|
+
if (state.cmp !== "cookiebot" && state.cmp !== "external" && state.mode === "off") {
|
|
182
196
|
return true;
|
|
183
197
|
}
|
|
184
198
|
if (requiredConsent.length === 0) {
|
|
@@ -186,6 +200,10 @@ function isConsentAllowed({
|
|
|
186
200
|
}
|
|
187
201
|
return requiredConsent.every((category) => state.categories[category]);
|
|
188
202
|
}
|
|
203
|
+
subscribeToConsentDelegation(() => {
|
|
204
|
+
snapshot = null;
|
|
205
|
+
notify();
|
|
206
|
+
});
|
|
189
207
|
export {
|
|
190
208
|
consentStore,
|
|
191
209
|
isConsentAllowed,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/consent-store.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory, ConsentMode } from \"schemas/generated/consent\";\nimport type { ConsentCategories } from \"./cookie\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { deriveConsentAction } from \"./consent-action\";\nimport { recordConsentAction } from \"./consent-actions\";\nimport {\n CONSENT_COOKIE_MAX_AGE_SECONDS,\n CONSENT_COOKIE_NAME,\n DENIED_CATEGORIES,\n GRANTED_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookieValue,\n} from \"./cookie\";\n\n/**\n * Framework-agnostic, cookie-backed consent store. Lives as a single module\n * singleton so `ReploScripts`, the site's consent banner, and `AnalyticsProvider`\n * all share the same state regardless of where they sit in the React tree \u2014 no\n * provider-ancestor relationship is required.\n *\n * Consent-dependent work (script injection, banner visibility) happens on the\n * client (effects / post-mount), so there is no SSR/hydration mismatch. The\n * client reads the (non-httpOnly) consent cookie directly, so no server seed is\n * needed. The whole-site `mode` comes from the `data-replo-consent-mode`\n * attribute on `<html>`.\n */\n\nexport interface ConsentState {\n mode: ConsentMode;\n categories: ConsentCategories;\n /** Epoch ms when the visitor last made a choice, or null if undecided. */\n decidedAt: number | null;\n /**\n * Persistent pseudonymous per-browser id linking a visitor's consent history.\n * Absent until the first `commit()` (and on legacy pre-id cookies, until the\n * next interaction lazily backfills it).\n */\n consentId?: string;\n}\n\n// The non-necessary categories a visitor can grant or revoke. `necessary` is\n// always granted and never toggled.\nconst TOGGLEABLE_CATEGORIES: Exclude<ConsentCategory, \"necessary\">[] = [\n \"analytics\",\n \"marketing\",\n \"preferences\",\n \"sale_of_data\",\n];\n\n// A stable reference for SSR / first hydration render. Consent-dependent DOM is\n// only produced on the client, so this constant never causes a mismatch.\nconst SERVER_SNAPSHOT: ConsentState = Object.freeze({\n mode: \"off\",\n categories: DENIED_CATEGORIES,\n decidedAt: null,\n});\n\nconst listeners = new Set<() => void>();\nlet snapshot: ConsentState | null = null;\n\nfunction isConsentMode(value: string | null | undefined): value is ConsentMode {\n return value === \"off\" || value === \"simple\" || value === \"per-category\";\n}\n\nfunction resolveMode(): ConsentMode {\n if (typeof window === \"undefined\") {\n return \"off\";\n }\n const attribute = document.documentElement.dataset.reploConsentMode;\n return isConsentMode(attribute) ? attribute : \"off\";\n}\n\n// Used when the consent caller doesn't declare a policy version via\n// `useConsent({ version })` or the store method options.\nconst DEFAULT_CONSENT_POLICY_VERSION = 1;\n\nfunction computeSnapshot(): ConsentState {\n if (typeof window === \"undefined\") {\n return SERVER_SNAPSHOT;\n }\n\n const mode = resolveMode();\n\n const fromCookie = parseConsentCookie(document.cookie);\n if (fromCookie) {\n return {\n mode,\n categories: {\n necessary: true,\n analytics: fromCookie.categories.analytics,\n marketing: fromCookie.categories.marketing,\n preferences: fromCookie.categories.preferences,\n sale_of_data: fromCookie.categories.sale_of_data,\n },\n decidedAt: fromCookie.decidedAt,\n consentId: fromCookie.consentId,\n };\n }\n\n return { mode, categories: DENIED_CATEGORIES, decidedAt: null };\n}\n\nfunction writeCookie(\n categories: ConsentCategories,\n decidedAt: number,\n consentId: string,\n) {\n if (typeof document === \"undefined\") {\n return;\n }\n const value = serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n });\n document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\nfunction notify() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction commit({\n categories,\n policyVersion = DEFAULT_CONSENT_POLICY_VERSION,\n}: {\n categories: ConsentCategories;\n policyVersion?: number;\n}) {\n const previous = consentStore.getSnapshot();\n const decidedAt = Date.now();\n // Reuse the visitor's existing id, or mint one on the first decision (also\n // lazily backfills legacy pre-id cookies). `crypto.randomUUID` is available in\n // every browser we target and on the server (where commit() never runs).\n const consentId = previous.consentId ?? crypto.randomUUID();\n const resultingCategories: ConsentCategories = {\n ...categories,\n necessary: true,\n };\n writeCookie(categories, decidedAt, consentId);\n snapshot = {\n mode: previous.mode,\n categories: resultingCategories,\n decidedAt,\n consentId,\n };\n notify();\n\n // Revocation cannot unload an already-loaded vendor. If the visitor had\n // previously decided and is now turning a granted category off, reload so the\n // page renders without those scripts. Deferred until after the audit record is\n // sent so the reload doesn't cancel the in-flight server action.\n const wasDowngrade =\n previous.decidedAt !== null &&\n TOGGLEABLE_CATEGORIES.some((category) => {\n return previous.categories[category] && !categories[category];\n });\n const reloadIfDowngraded = () => {\n if (wasDowngrade && typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n // Proof-of-consent audit record (ungated by consent, but only on managed\n // sites). On `mode === \"off\"` there is no banner/consent UX, so a\n // `setTrackingConsent()` call from a third-party script would only produce\n // meaningless `consent_mode: \"off\"` noise \u2014 skip it. Egress runs through a\n // server action (like the cart) so the client never needs the project id or\n // analytics-fire URL.\n if (previous.mode !== \"off\") {\n void recordConsentAction({\n consentId,\n action: deriveConsentAction({ previous, next: resultingCategories }),\n categories: resultingCategories,\n consentMode: previous.mode,\n decidedAt,\n policyVersion,\n }).finally(reloadIfDowngraded);\n } else {\n reloadIfDowngraded();\n }\n}\n\nexport const consentStore = {\n getSnapshot(): ConsentState {\n if (!snapshot) {\n snapshot = computeSnapshot();\n }\n return snapshot;\n },\n getServerSnapshot(): ConsentState {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Grant every consent category. */\n accept(options: { policyVersion?: number } = {}) {\n commit({\n categories: GRANTED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Deny every non-necessary category. */\n reject(options: { policyVersion?: number } = {}) {\n commit({\n categories: DENIED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Set a subset of categories, leaving the rest at their current value. */\n updateCategories({\n next,\n policyVersion,\n }: {\n next: Partial<ConsentCategories>;\n policyVersion?: number;\n }) {\n const current = consentStore.getSnapshot().categories;\n commit({\n categories: { ...current, ...next, necessary: true },\n policyVersion,\n });\n },\n /** Test-only: reset cached state so the next read recomputes. */\n reset() {\n snapshot = null;\n },\n};\n\nexport interface UseConsentOptions {\n /**\n * Site-owned consent policy version, stamped onto each proof-of-consent\n * record so a decision can be tied to the exact banner the visitor saw.\n * Bump it whenever the banner copy or category set changes. Defaults to 1.\n */\n version?: number;\n}\n\nexport interface UseConsentResult extends ConsentState {\n accept(): void;\n reject(): void;\n updateCategories(next: Partial<ConsentCategories>): void;\n}\n\nexport function useConsent(options: UseConsentOptions = {}): UseConsentResult {\n const policyVersion = options.version;\n const state = useSyncExternalStore(\n consentStore.subscribe,\n consentStore.getSnapshot,\n consentStore.getServerSnapshot,\n );\n return {\n ...state,\n accept: () => consentStore.accept({ policyVersion }),\n reject: () => consentStore.reject({ policyVersion }),\n updateCategories: (next) => {\n consentStore.updateCategories({ next, policyVersion });\n },\n };\n}\n\n/**\n * Whether a script requiring `requiredConsent` is allowed to load right now.\n * `mode === \"off\"` always allows; otherwise every required category must be\n * granted. An empty requirement list is always allowed.\n */\nexport function isConsentAllowed({\n state,\n requiredConsent,\n}: {\n state: ConsentState;\n requiredConsent: ConsentCategory[];\n}): boolean {\n if (state.mode === \"off\") {\n return true;\n }\n if (requiredConsent.length === 0) {\n return true;\n }\n return requiredConsent.every((category) => state.categories[category]);\n}\n"],
|
|
5
|
-
"mappings": ";AAKA,SAAS,4BAA4B;AAErC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory, ConsentMode } from \"schemas/generated/consent\";\nimport type { ConsentCategories } from \"./cookie\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { deriveConsentAction } from \"./consent-action\";\nimport { recordConsentAction } from \"./consent-actions\";\nimport {\n findDelegatedPlatform,\n getDelegatedConsent,\n OPTIONAL_CONSENT_CATEGORIES,\n subscribeToConsentDelegation,\n} from \"./consent-platform\";\nimport {\n CONSENT_COOKIE_MAX_AGE_SECONDS,\n CONSENT_COOKIE_NAME,\n DENIED_CATEGORIES,\n GRANTED_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookieValue,\n} from \"./cookie\";\n\n/**\n * Framework-agnostic, cookie-backed consent store. Lives as a single module\n * singleton so `ReploScripts`, the site's consent banner, and `AnalyticsProvider`\n * all share the same state regardless of where they sit in the React tree \u2014 no\n * provider-ancestor relationship is required.\n *\n * Consent-dependent work (script injection, banner visibility) happens on the\n * client (effects / post-mount), so there is no SSR/hydration mismatch. The\n * client reads the (non-httpOnly) consent cookie directly, so no server seed is\n * needed. The whole-site `mode` comes from the `data-replo-consent-mode`\n * attribute on `<html>`.\n */\n\nexport interface ConsentState {\n /** Which platform owns consent. Absent means Replo's native banner; \"external\" is a generic `consentPlatform` entry. */\n cmp?: \"replo\" | \"cookiebot\" | \"external\";\n mode: ConsentMode;\n categories: ConsentCategories;\n /** Epoch ms when the visitor last made a choice, or null if undecided. */\n decidedAt: number | null;\n /**\n * Persistent pseudonymous per-browser id linking a visitor's consent history.\n * Absent until the first `commit()` (and on legacy pre-id cookies, until the\n * next interaction lazily backfills it).\n */\n consentId?: string;\n}\n\n// A stable reference for SSR / first hydration render. Consent-dependent DOM is\n// only produced on the client, so this constant never causes a mismatch.\nconst SERVER_SNAPSHOT: ConsentState = Object.freeze({\n cmp: \"replo\",\n mode: \"off\",\n categories: DENIED_CATEGORIES,\n decidedAt: null,\n});\n\nconst listeners = new Set<() => void>();\nlet snapshot: ConsentState | null = null;\n\nfunction isConsentMode(value: string | null | undefined): value is ConsentMode {\n return value === \"off\" || value === \"simple\" || value === \"per-category\";\n}\n\nfunction resolveMode(): ConsentMode {\n if (typeof window === \"undefined\") {\n return \"off\";\n }\n const attribute = document.documentElement.dataset.reploConsentMode;\n return isConsentMode(attribute) ? attribute : \"off\";\n}\n\n// Used when the consent caller doesn't declare a policy version via\n// `useConsent({ version })` or the store method options.\nconst DEFAULT_CONSENT_POLICY_VERSION = 1;\n\nfunction computeSnapshot(): ConsentState {\n if (typeof window === \"undefined\") {\n return SERVER_SNAPSHOT;\n }\n\n const mode = resolveMode();\n\n // Delegated sites turn the native banner off, so `mode` alone would allow everything.\n const platform = findDelegatedPlatform();\n if (platform) {\n return {\n cmp: platform.isCookiebot ? \"cookiebot\" : \"external\",\n mode,\n ...getDelegatedConsent(),\n };\n }\n\n const fromCookie = parseConsentCookie(document.cookie);\n if (fromCookie) {\n return {\n cmp: \"replo\",\n mode,\n categories: {\n necessary: true,\n analytics: fromCookie.categories.analytics,\n marketing: fromCookie.categories.marketing,\n preferences: fromCookie.categories.preferences,\n sale_of_data: fromCookie.categories.sale_of_data,\n },\n decidedAt: fromCookie.decidedAt,\n consentId: fromCookie.consentId,\n };\n }\n\n return { cmp: \"replo\", mode, categories: DENIED_CATEGORIES, decidedAt: null };\n}\n\nfunction writeCookie(\n categories: ConsentCategories,\n decidedAt: number,\n consentId: string,\n) {\n if (typeof document === \"undefined\") {\n return;\n }\n const value = serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n });\n document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\nfunction notify() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction commit({\n categories,\n policyVersion = DEFAULT_CONSENT_POLICY_VERSION,\n}: {\n categories: ConsentCategories;\n policyVersion?: number;\n}) {\n const previous = consentStore.getSnapshot();\n // The platform owns the decision, so a native accept would write a cookie nothing reads.\n if (previous.cmp === \"cookiebot\" || previous.cmp === \"external\") {\n return;\n }\n const decidedAt = Date.now();\n // Reuse the visitor's existing id, or mint one on the first decision (also\n // lazily backfills legacy pre-id cookies). `crypto.randomUUID` is available in\n // every browser we target and on the server (where commit() never runs).\n const consentId = previous.consentId ?? crypto.randomUUID();\n const resultingCategories: ConsentCategories = {\n ...categories,\n necessary: true,\n };\n writeCookie(categories, decidedAt, consentId);\n snapshot = {\n cmp: previous.cmp,\n mode: previous.mode,\n categories: resultingCategories,\n decidedAt,\n consentId,\n };\n notify();\n\n // Revocation cannot unload an already-loaded vendor. If the visitor had\n // previously decided and is now turning a granted category off, reload so the\n // page renders without those scripts. Deferred until after the audit record is\n // sent so the reload doesn't cancel the in-flight server action.\n const wasDowngrade =\n previous.decidedAt !== null &&\n OPTIONAL_CONSENT_CATEGORIES.some((category) => {\n return previous.categories[category] && !categories[category];\n });\n const reloadIfDowngraded = () => {\n if (wasDowngrade && typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n // Proof-of-consent audit record (ungated by consent, but only on managed\n // sites). On `mode === \"off\"` there is no banner/consent UX, so a\n // `setTrackingConsent()` call from a third-party script would only produce\n // meaningless `consent_mode: \"off\"` noise \u2014 skip it. Egress runs through a\n // server action (like the cart) so the client never needs the project id or\n // analytics-fire URL.\n if (previous.mode !== \"off\") {\n void recordConsentAction({\n consentId,\n action: deriveConsentAction({ previous, next: resultingCategories }),\n categories: resultingCategories,\n consentMode: previous.mode,\n decidedAt,\n policyVersion,\n }).finally(reloadIfDowngraded);\n } else {\n reloadIfDowngraded();\n }\n}\n\nexport const consentStore = {\n getSnapshot(): ConsentState {\n // Delegation is published mid-render, so a snapshot read earlier can predate it.\n if (\n !snapshot ||\n (findDelegatedPlatform() !== null &&\n (snapshot.cmp ?? \"replo\") === \"replo\")\n ) {\n snapshot = computeSnapshot();\n }\n return snapshot;\n },\n getServerSnapshot(): ConsentState {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Grant every consent category. */\n accept(options: { policyVersion?: number } = {}) {\n commit({\n categories: GRANTED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Deny every non-necessary category. */\n reject(options: { policyVersion?: number } = {}) {\n commit({\n categories: DENIED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Set a subset of categories, leaving the rest at their current value. */\n updateCategories({\n next,\n policyVersion,\n }: {\n next: Partial<ConsentCategories>;\n policyVersion?: number;\n }) {\n const current = consentStore.getSnapshot().categories;\n commit({\n categories: { ...current, ...next, necessary: true },\n policyVersion,\n });\n },\n /** Test-only: reset cached state so the next read recomputes. */\n reset() {\n snapshot = null;\n },\n};\n\nexport interface UseConsentOptions {\n /**\n * Site-owned consent policy version, stamped onto each proof-of-consent\n * record so a decision can be tied to the exact banner the visitor saw.\n * Bump it whenever the banner copy or category set changes. Defaults to 1.\n */\n version?: number;\n}\n\nexport interface UseConsentResult extends ConsentState {\n accept(): void;\n reject(): void;\n updateCategories(next: Partial<ConsentCategories>): void;\n}\n\nexport function useConsent(options: UseConsentOptions = {}): UseConsentResult {\n const policyVersion = options.version;\n const state = useSyncExternalStore(\n consentStore.subscribe,\n consentStore.getSnapshot,\n consentStore.getServerSnapshot,\n );\n return {\n ...state,\n accept: () => consentStore.accept({ policyVersion }),\n reject: () => consentStore.reject({ policyVersion }),\n updateCategories: (next) => {\n consentStore.updateCategories({ next, policyVersion });\n },\n };\n}\n\n/**\n * Whether a script requiring `requiredConsent` may load now. `mode === \"off\"`\n * allows everything only under Replo's banner; a delegated CMP answers instead.\n */\nexport function isConsentAllowed({\n state,\n requiredConsent,\n}: {\n state: ConsentState;\n requiredConsent: ConsentCategory[];\n}): boolean {\n if (\n state.cmp !== \"cookiebot\" &&\n state.cmp !== \"external\" &&\n state.mode === \"off\"\n ) {\n return true;\n }\n if (requiredConsent.length === 0) {\n return true;\n }\n return requiredConsent.every((category) => state.categories[category]);\n}\n\n// The platform answers asynchronously; invalidate so every consent gate re-evaluates.\nsubscribeToConsentDelegation(() => {\n snapshot = null;\n notify();\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,7 +5,9 @@ import type { ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
|
5
5
|
* their removal. Lives outside replo-scripts.tsx so the component file only
|
|
6
6
|
* exports components (HMR) and this stays off the published public surface.
|
|
7
7
|
*/
|
|
8
|
-
export declare function injectScriptDescriptors({ baseId, descriptors, }: {
|
|
8
|
+
export declare function injectScriptDescriptors({ baseId, descriptors, extraAttributes, }: {
|
|
9
9
|
baseId: string;
|
|
10
10
|
descriptors: ScriptTagDescriptor[];
|
|
11
|
+
/** Applied last, so a consent platform's `text/plain` beats a descriptor's own type. */
|
|
12
|
+
extraAttributes?: Record<string, string>;
|
|
11
13
|
}): HTMLScriptElement[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
function injectScriptDescriptors({
|
|
2
2
|
baseId,
|
|
3
|
-
descriptors
|
|
3
|
+
descriptors,
|
|
4
|
+
extraAttributes
|
|
4
5
|
}) {
|
|
5
6
|
const createdNodes = [];
|
|
6
7
|
descriptors.forEach((descriptor, index) => {
|
|
@@ -18,17 +19,32 @@ function injectScriptDescriptors({
|
|
|
18
19
|
)) {
|
|
19
20
|
scriptElement.setAttribute(attributeName, attributeValue);
|
|
20
21
|
}
|
|
22
|
+
} else if (descriptor.module && extraAttributes?.type === "text/plain") {
|
|
23
|
+
scriptElement.textContent = buildModuleShim(descriptor.body);
|
|
21
24
|
} else {
|
|
22
25
|
if (descriptor.module) {
|
|
23
26
|
scriptElement.type = "module";
|
|
24
27
|
}
|
|
25
28
|
scriptElement.textContent = descriptor.body;
|
|
26
29
|
}
|
|
30
|
+
for (const [attributeName, attributeValue] of Object.entries(
|
|
31
|
+
extraAttributes ?? {}
|
|
32
|
+
)) {
|
|
33
|
+
scriptElement.setAttribute(attributeName, attributeValue);
|
|
34
|
+
}
|
|
27
35
|
document.head.append(scriptElement);
|
|
28
36
|
createdNodes.push(scriptElement);
|
|
29
37
|
});
|
|
30
38
|
return createdNodes;
|
|
31
39
|
}
|
|
40
|
+
function buildModuleShim(body) {
|
|
41
|
+
return [
|
|
42
|
+
'var moduleScript = document.createElement("script");',
|
|
43
|
+
'moduleScript.type = "module";',
|
|
44
|
+
`moduleScript.textContent = ${JSON.stringify(body)};`,
|
|
45
|
+
"document.head.append(moduleScript);"
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
32
48
|
export {
|
|
33
49
|
injectScriptDescriptors
|
|
34
50
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/inject-script-descriptors.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\n/**\n * Creates and appends the script nodes for `descriptors`, skipping node ids\n * already in the document, and returns the created nodes so the caller owns\n * their removal. Lives outside replo-scripts.tsx so the component file only\n * exports components (HMR) and this stays off the published public surface.\n */\nexport function injectScriptDescriptors({\n baseId,\n descriptors,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n}): HTMLScriptElement[] {\n const createdNodes: HTMLScriptElement[] = [];\n descriptors.forEach((descriptor, index) => {\n const nodeId = `${baseId}#${index}`;\n if (document.querySelector(`script[data-replo-script-id=\"${nodeId}\"]`)) {\n return;\n }\n const scriptElement = document.createElement(\"script\");\n scriptElement.dataset.reploScriptId = nodeId;\n if (descriptor.kind === \"external\") {\n scriptElement.src = descriptor.src;\n scriptElement.async = true;\n for (const [attributeName, attributeValue] of Object.entries(\n descriptor.attributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n } else {\n // Module snippets (top-level await, import()) are a SyntaxError as\n // classic scripts, so the type must survive injection.\n if (descriptor.module) {\n scriptElement.type = \"module\";\n }\n scriptElement.textContent = descriptor.body;\n }\n document.head.append(scriptElement);\n createdNodes.push(scriptElement);\n });\n return createdNodes;\n}\n"],
|
|
5
|
-
"mappings": "AAQO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,
|
|
4
|
+
"sourcesContent": ["import type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\n/**\n * Creates and appends the script nodes for `descriptors`, skipping node ids\n * already in the document, and returns the created nodes so the caller owns\n * their removal. Lives outside replo-scripts.tsx so the component file only\n * exports components (HMR) and this stays off the published public surface.\n */\nexport function injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Applied last, so a consent platform's `text/plain` beats a descriptor's own type. */\n extraAttributes?: Record<string, string>;\n}): HTMLScriptElement[] {\n const createdNodes: HTMLScriptElement[] = [];\n descriptors.forEach((descriptor, index) => {\n const nodeId = `${baseId}#${index}`;\n if (document.querySelector(`script[data-replo-script-id=\"${nodeId}\"]`)) {\n return;\n }\n const scriptElement = document.createElement(\"script\");\n scriptElement.dataset.reploScriptId = nodeId;\n if (descriptor.kind === \"external\") {\n scriptElement.src = descriptor.src;\n scriptElement.async = true;\n for (const [attributeName, attributeValue] of Object.entries(\n descriptor.attributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n } else if (descriptor.module && extraAttributes?.type === \"text/plain\") {\n // A consent platform restores blocked tags as CLASSIC scripts, which is a\n // SyntaxError for module source. The blocked body is a classic shim that\n // re-creates the real module tag when the platform activates it.\n scriptElement.textContent = buildModuleShim(descriptor.body);\n } else {\n // Module snippets (top-level await, import()) are a SyntaxError as\n // classic scripts, so the type must survive injection.\n if (descriptor.module) {\n scriptElement.type = \"module\";\n }\n scriptElement.textContent = descriptor.body;\n }\n for (const [attributeName, attributeValue] of Object.entries(\n extraAttributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n document.head.append(scriptElement);\n createdNodes.push(scriptElement);\n });\n return createdNodes;\n}\n\nfunction buildModuleShim(body: string): string {\n return [\n 'var moduleScript = document.createElement(\"script\");',\n 'moduleScript.type = \"module\";',\n `moduleScript.textContent = ${JSON.stringify(body)};`,\n \"document.head.append(moduleScript);\",\n ].join(\"\\n\");\n}\n"],
|
|
5
|
+
"mappings": "AAQO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,GAKwB;AACtB,QAAM,eAAoC,CAAC;AAC3C,cAAY,QAAQ,CAAC,YAAY,UAAU;AACzC,UAAM,SAAS,GAAG,MAAM,IAAI,KAAK;AACjC,QAAI,SAAS,cAAc,gCAAgC,MAAM,IAAI,GAAG;AACtE;AAAA,IACF;AACA,UAAM,gBAAgB,SAAS,cAAc,QAAQ;AACrD,kBAAc,QAAQ,gBAAgB;AACtC,QAAI,WAAW,SAAS,YAAY;AAClC,oBAAc,MAAM,WAAW;AAC/B,oBAAc,QAAQ;AACtB,iBAAW,CAAC,eAAe,cAAc,KAAK,OAAO;AAAA,QACnD,WAAW,cAAc,CAAC;AAAA,MAC5B,GAAG;AACD,sBAAc,aAAa,eAAe,cAAc;AAAA,MAC1D;AAAA,IACF,WAAW,WAAW,UAAU,iBAAiB,SAAS,cAAc;AAItE,oBAAc,cAAc,gBAAgB,WAAW,IAAI;AAAA,IAC7D,OAAO;AAGL,UAAI,WAAW,QAAQ;AACrB,sBAAc,OAAO;AAAA,MACvB;AACA,oBAAc,cAAc,WAAW;AAAA,IACzC;AACA,eAAW,CAAC,eAAe,cAAc,KAAK,OAAO;AAAA,MACnD,mBAAmB,CAAC;AAAA,IACtB,GAAG;AACD,oBAAc,aAAa,eAAe,cAAc;AAAA,IAC1D;AACA,aAAS,KAAK,OAAO,aAAa;AAClC,iBAAa,KAAK,aAAa;AAAA,EACjC,CAAC;AACD,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAsB;AAC7C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,8BAA8B,KAAK,UAAU,IAAI,CAAC;AAAA,IAClD;AAAA,EACF,EAAE,KAAK,IAAI;AACb;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|