@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.
Files changed (46) hide show
  1. package/_vendor/replo-utils/lib/url.d.ts +5 -0
  2. package/_vendor/schemas/generated/consent.d.ts +18 -1
  3. package/_vendor/schemas/routing/locale.d.ts +45 -0
  4. package/_vendor/schemas/routing/locale.mjs +95 -0
  5. package/_vendor/schemas/routing/localeNegotiation.d.ts +87 -0
  6. package/_vendor/schemas/routing/localeNegotiation.mjs +141 -0
  7. package/_vendor/schemas/routing/rules.mjs +1 -1
  8. package/analytics/analytics-provider.js +2 -0
  9. package/analytics/analytics-provider.js.map +2 -2
  10. package/analytics/get-analytics-sinks.d.ts +1 -0
  11. package/analytics/get-analytics-sinks.js +4 -1
  12. package/analytics/get-analytics-sinks.js.map +2 -2
  13. package/analytics/replo-pixel-script.d.ts +4 -0
  14. package/analytics/replo-pixel-script.js +10 -2
  15. package/analytics/replo-pixel-script.js.map +2 -2
  16. package/analytics/utils/analytics-utils.js +5 -6
  17. package/analytics/utils/analytics-utils.js.map +2 -2
  18. package/consent/consent-platform.d.ts +52 -0
  19. package/consent/consent-platform.js +223 -0
  20. package/consent/consent-platform.js.map +7 -0
  21. package/consent/consent-store.d.ts +4 -3
  22. package/consent/consent-store.js +28 -10
  23. package/consent/consent-store.js.map +2 -2
  24. package/consent/inject-script-descriptors.d.ts +3 -1
  25. package/consent/inject-script-descriptors.js +17 -1
  26. package/consent/inject-script-descriptors.js.map +2 -2
  27. package/consent/replo-scripts.d.ts +9 -2
  28. package/consent/replo-scripts.js +66 -7
  29. package/consent/replo-scripts.js.map +2 -2
  30. package/consent/script-snippets.d.ts +1 -1
  31. package/consent/script-snippets.js.map +2 -2
  32. package/consent/types.d.ts +13 -2
  33. package/consent/window-api.d.ts +4 -1
  34. package/consent/window-api.js +3 -0
  35. package/consent/window-api.js.map +2 -2
  36. package/lib/buildMetadata.js +3 -3
  37. package/package.json +14 -4
  38. package/routing/evaluate.d.ts +3 -1
  39. package/routing/evaluate.js +150 -6
  40. package/routing/evaluate.js.map +3 -3
  41. package/routing/geo.d.ts +9 -0
  42. package/routing/geo.js +17 -0
  43. package/routing/geo.js.map +7 -0
  44. package/routing/locale.d.ts +13 -0
  45. package/routing/locale.js +13 -0
  46. package/routing/locale.js.map +7 -0
@@ -1,4 +1,4 @@
1
- import type { ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
1
+ import type { ConsentCategory, ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
2
2
  /**
3
3
  * Injects the resolved tags into `document.head` once a caller has decided
4
4
  * consent allows it. Reused both for author-managed `ReploScripts` entries and
@@ -9,14 +9,21 @@ import type { ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/g
9
9
  * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict
10
10
  * Mode double-invokes do not double-inject.
11
11
  */
12
- export declare function InjectedScript({ baseId, descriptors, }: {
12
+ export declare function InjectedScript({ baseId, descriptors, requiredConsent, }: {
13
13
  baseId: string;
14
14
  descriptors: ScriptTagDescriptor[];
15
+ /** Used under a delegated platform to derive the attributes that gate the tag. */
16
+ requiredConsent?: ConsentCategory[];
15
17
  }): null;
16
18
  /**
17
19
  * The single registry component for all managed tracking scripts on the site.
18
20
  * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`
19
21
  * array. Each entry registers itself and is gated + injected per consent.
22
+ *
23
+ * A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes
24
+ * over consent site-wide: scripts carry the platform's blocking attributes, its
25
+ * loader injects after them so its startup scan sees the full set, and Replo's
26
+ * own gates follow the platform instead of the native banner.
20
27
  */
21
28
  export declare function ReploScripts({ scripts }: {
22
29
  scripts: ReploScriptEntry[];
@@ -1,6 +1,16 @@
1
1
  "use client";
2
- import { Fragment, jsx } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect } from "react";
4
+ import {
5
+ activateBlockedScripts,
6
+ buildGrantMarker,
7
+ enableConsentDelegation,
8
+ findConsentPlatform,
9
+ findDelegatedPlatform,
10
+ getBlockingAttributes,
11
+ isConsentPlatformEntry,
12
+ OPTIONAL_CONSENT_CATEGORIES
13
+ } from "./consent-platform";
4
14
  import { isConsentAllowed, useConsent } from "./consent-store";
5
15
  import { injectScriptDescriptors } from "./inject-script-descriptors";
6
16
  import { registerScript, unregisterScript } from "./script-registration-store";
@@ -13,12 +23,18 @@ function scriptId(entry) {
13
23
  if (entry.type === "snippet") {
14
24
  return `snippet:${entry.id}`;
15
25
  }
26
+ if (entry.type === "consentPlatform") {
27
+ return `consent-platform:${entry.id}`;
28
+ }
16
29
  return `${entry.type}:${entry.identifier}`;
17
30
  }
18
31
  function requiredConsentFor(entry) {
19
32
  if (entry.type === "custom" || entry.type === "snippet") {
20
33
  return entry.requiredConsent;
21
34
  }
35
+ if (entry.type === "consentPlatform" || entry.type === "Cookiebot") {
36
+ return [];
37
+ }
22
38
  return entry.requiredConsent ?? defaultConsentFor(entry.type);
23
39
  }
24
40
  function descriptorsFor(entry) {
@@ -34,18 +50,32 @@ function descriptorsFor(entry) {
34
50
  if (entry.type === "snippet") {
35
51
  return [{ kind: "inline", body: entry.body, module: entry.module }];
36
52
  }
53
+ if (entry.type === "consentPlatform" || entry.type === "Cookiebot") {
54
+ return [];
55
+ }
37
56
  return buildScriptTags({ type: entry.type, identifier: entry.identifier });
38
57
  }
39
58
  function InjectedScript({
40
59
  baseId,
41
- descriptors
60
+ descriptors,
61
+ requiredConsent = []
42
62
  }) {
43
- const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;
63
+ const injectionKey = `${baseId}|${JSON.stringify(descriptors)}|${requiredConsent.join(",")}`;
44
64
  useEffect(() => {
45
65
  if (typeof document === "undefined") {
46
66
  return;
47
67
  }
48
- const createdNodes = injectScriptDescriptors({ baseId, descriptors });
68
+ const platform = findDelegatedPlatform();
69
+ const blockingAttributes = platform ? getBlockingAttributes(platform, requiredConsent) : {};
70
+ const blockedByPlatform = "type" in blockingAttributes;
71
+ const createdNodes = injectScriptDescriptors({
72
+ baseId,
73
+ descriptors,
74
+ extraAttributes: blockingAttributes
75
+ });
76
+ if (blockedByPlatform && createdNodes.length > 0) {
77
+ activateBlockedScripts();
78
+ }
49
79
  return () => {
50
80
  for (const node of createdNodes) {
51
81
  node.remove();
@@ -63,14 +93,43 @@ function ManagedScript({ entry }) {
63
93
  registerScript({ id, type: entry.type, requiredConsent });
64
94
  return () => unregisterScript(id);
65
95
  }, [id, entry.type, consentKey]);
66
- if (!isConsentAllowed({ state: consent, requiredConsent })) {
96
+ if (!findDelegatedPlatform() && !isConsentAllowed({ state: consent, requiredConsent })) {
67
97
  return null;
68
98
  }
69
- return /* @__PURE__ */ jsx(InjectedScript, { baseId: id, descriptors: descriptorsFor(entry) });
99
+ return /* @__PURE__ */ jsx(
100
+ InjectedScript,
101
+ {
102
+ baseId: id,
103
+ descriptors: descriptorsFor(entry),
104
+ requiredConsent
105
+ }
106
+ );
70
107
  }
71
108
  function ReploScripts({ scripts }) {
72
109
  useEffect(() => installConsentWindowApi(), []);
73
- return /* @__PURE__ */ jsx(Fragment, { children: scripts.map((entry) => /* @__PURE__ */ jsx(ManagedScript, { entry }, scriptId(entry))) });
110
+ const platform = findConsentPlatform(scripts);
111
+ if (platform) {
112
+ enableConsentDelegation(platform);
113
+ }
114
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ scripts.filter((entry) => !isConsentPlatformEntry(entry)).map((entry) => /* @__PURE__ */ jsx(ManagedScript, { entry }, scriptId(entry))),
116
+ platform && !platform.isCookiebot && OPTIONAL_CONSENT_CATEGORIES.map((category) => /* @__PURE__ */ jsx(
117
+ InjectedScript,
118
+ {
119
+ baseId: `consent-platform:${platform.id}:grant:${category}`,
120
+ descriptors: [buildGrantMarker(category)],
121
+ requiredConsent: [category]
122
+ },
123
+ category
124
+ )),
125
+ platform && /* @__PURE__ */ jsx(
126
+ InjectedScript,
127
+ {
128
+ baseId: `consent-platform:${platform.id}`,
129
+ descriptors: platform.loader
130
+ }
131
+ )
132
+ ] });
74
133
  }
75
134
  export {
76
135
  InjectedScript,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../consent/replo-scripts.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\nimport { useEffect } from \"react\";\n\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\nexport function InjectedScript({\n baseId,\n descriptors,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n}) {\n // Re-inject only when the resolved tags actually change.\n const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;\n\n // eslint-disable-next-line replo/no-use-effect -- script injection is a DOM side effect that must run after mount and on consent changes\n useEffect(() => {\n if (typeof document === \"undefined\") {\n return;\n }\n const createdNodes = injectScriptDescriptors({ baseId, descriptors });\n\n return () => {\n // Removing the node does not unload an already-loaded vendor (see the plan's\n // revocation note); full teardown is a reload triggered by the banner. This\n // cleanup keeps the DOM tidy and prevents duplicates across remounts.\n for (const node of createdNodes) {\n node.remove();\n }\n };\n }, [injectionKey]);\n\n return null;\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const consentKey = requiredConsent.join(\",\");\n\n // eslint-disable-next-line replo/no-use-effect -- register/unregister with the singleton so the analytics provider can gate sinks by what's on the page\n useEffect(() => {\n registerScript({ id, type: entry.type, requiredConsent });\n return () => unregisterScript(id);\n }, [id, entry.type, consentKey]);\n\n if (!isConsentAllowed({ state: consent, requiredConsent })) {\n return null;\n }\n return <InjectedScript baseId={id} descriptors={descriptorsFor(entry)} />;\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // eslint-disable-next-line replo/no-use-effect -- install the window.Replo.customerPrivacy API + change event once the consent runtime mounts\n useEffect(() => installConsentWindowApi(), []);\n\n return (\n <>\n {scripts.map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n </>\n );\n}\n"],
5
- "mappings": ";AAkHS,SAaL,UAbK;AA1GT,SAAS,iBAAiB;AAE1B,SAAS,kBAAkB,kBAAkB;AAC7C,SAAS,+BAA+B;AACxC,SAAS,gBAAgB,wBAAwB;AACjD,SAAS,iBAAiB,yBAAyB;AACnD,SAAS,+BAA+B;AAMxC,SAAS,SAAS,OAAiC;AACjD,MAAI,MAAM,SAAS,UAAU;AAC3B,WAAO,UAAU,MAAM,EAAE;AAAA,EAC3B;AACA,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,WAAW,MAAM,EAAE;AAAA,EAC5B;AACA,SAAO,GAAG,MAAM,IAAI,IAAI,MAAM,UAAU;AAC1C;AAEA,SAAS,mBAAmB,OAA4C;AACtE,MAAI,MAAM,SAAS,YAAY,MAAM,SAAS,WAAW;AACvD,WAAO,MAAM;AAAA,EACf;AACA,SAAO,MAAM,mBAAmB,kBAAkB,MAAM,IAAI;AAC9D;AAQA,SAAS,eAAe,OAAgD;AACtE,MAAI,MAAM,SAAS,UAAU;AAC3B,QAAI,MAAM,KAAK;AACb,aAAO,CAAC,EAAE,MAAM,YAAY,KAAK,MAAM,IAAI,CAAC;AAAA,IAC9C;AACA,QAAI,MAAM,MAAM;AACd,aAAO,CAAC,EAAE,MAAM,UAAU,MAAM,MAAM,KAAK,CAAC;AAAA,IAC9C;AACA,WAAO,CAAC;AAAA,EACV;AACA,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,CAAC,EAAE,MAAM,UAAU,MAAM,MAAM,MAAM,QAAQ,MAAM,OAAO,CAAC;AAAA,EACpE;AACA,SAAO,gBAAgB,EAAE,MAAM,MAAM,MAAM,YAAY,MAAM,WAAW,CAAC;AAC3E;AAYO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AACF,GAGG;AAED,QAAM,eAAe,GAAG,MAAM,IAAI,KAAK,UAAU,WAAW,CAAC;AAG7D,YAAU,MAAM;AACd,QAAI,OAAO,aAAa,aAAa;AACnC;AAAA,IACF;AACA,UAAM,eAAe,wBAAwB,EAAE,QAAQ,YAAY,CAAC;AAEpE,WAAO,MAAM;AAIX,iBAAW,QAAQ,cAAc;AAC/B,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;AAEA,SAAS,cAAc,EAAE,MAAM,GAAgC;AAC7D,QAAM,UAAU,WAAW;AAC3B,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,QAAM,KAAK,SAAS,KAAK;AACzB,QAAM,aAAa,gBAAgB,KAAK,GAAG;AAG3C,YAAU,MAAM;AACd,mBAAe,EAAE,IAAI,MAAM,MAAM,MAAM,gBAAgB,CAAC;AACxD,WAAO,MAAM,iBAAiB,EAAE;AAAA,EAClC,GAAG,CAAC,IAAI,MAAM,MAAM,UAAU,CAAC;AAE/B,MAAI,CAAC,iBAAiB,EAAE,OAAO,SAAS,gBAAgB,CAAC,GAAG;AAC1D,WAAO;AAAA,EACT;AACA,SAAO,oBAAC,kBAAe,QAAQ,IAAI,aAAa,eAAe,KAAK,GAAG;AACzE;AAOO,SAAS,aAAa,EAAE,QAAQ,GAAoC;AAEzE,YAAU,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAE7C,SACE,gCACG,kBAAQ,IAAI,CAAC,UACZ,oBAAC,iBAAoC,SAAjB,SAAS,KAAK,CAAiB,CACpD,GACH;AAEJ;",
4
+ "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\nimport { useEffect } from \"react\";\n\nimport {\n activateBlockedScripts,\n buildGrantMarker,\n enableConsentDelegation,\n findConsentPlatform,\n findDelegatedPlatform,\n getBlockingAttributes,\n isConsentPlatformEntry,\n OPTIONAL_CONSENT_CATEGORIES,\n} from \"./consent-platform\";\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n if (entry.type === \"consentPlatform\") {\n return `consent-platform:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n // Platform entries are filtered out before render; arms exist for the types.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n // Platform entries are filtered out before render; the arm exists for the\n // types \u2014 the loader injects via the platform config, not per-entry.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\nexport function InjectedScript({\n baseId,\n descriptors,\n requiredConsent = [],\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Used under a delegated platform to derive the attributes that gate the tag. */\n requiredConsent?: ConsentCategory[];\n}) {\n // Re-inject only when the resolved tags actually change.\n const injectionKey = `${baseId}|${JSON.stringify(descriptors)}|${requiredConsent.join(\",\")}`;\n\n // eslint-disable-next-line replo/no-use-effect -- script injection is a DOM side effect that must run after mount and on consent changes\n useEffect(() => {\n if (typeof document === \"undefined\") {\n return;\n }\n // Resolved in the effect, not render: delegation is published mid-render, after\n // earlier-mounted tags (the first-party pixel) render but before any effect runs.\n const platform = findDelegatedPlatform();\n const blockingAttributes = platform\n ? getBlockingAttributes(platform, requiredConsent)\n : {};\n const blockedByPlatform = \"type\" in blockingAttributes;\n const createdNodes = injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes: blockingAttributes,\n });\n\n // Belt-and-suspenders for Cookiebot, whose loader may have scanned already.\n if (blockedByPlatform && createdNodes.length > 0) {\n activateBlockedScripts();\n }\n\n return () => {\n // Removing the node does not unload an already-loaded vendor (see the plan's\n // revocation note); full teardown is a reload triggered by the banner. This\n // cleanup keeps the DOM tidy and prevents duplicates across remounts.\n for (const node of createdNodes) {\n node.remove();\n }\n };\n }, [injectionKey]);\n\n return null;\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const consentKey = requiredConsent.join(\",\");\n\n // eslint-disable-next-line replo/no-use-effect -- register/unregister with the singleton so the analytics provider can gate sinks by what's on the page\n useEffect(() => {\n registerScript({ id, type: entry.type, requiredConsent });\n return () => unregisterScript(id);\n }, [id, entry.type, consentKey]);\n\n // Under a consent platform the tag is injected and the platform decides when\n // it runs.\n if (\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent })\n ) {\n return null;\n }\n return (\n <InjectedScript\n baseId={id}\n descriptors={descriptorsFor(entry)}\n requiredConsent={requiredConsent}\n />\n );\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n *\n * A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes\n * over consent site-wide: scripts carry the platform's blocking attributes, its\n * loader injects after them so its startup scan sees the full set, and Replo's\n * own gates follow the platform instead of the native banner.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // eslint-disable-next-line replo/no-use-effect -- install the window.Replo.customerPrivacy API + change event once the consent runtime mounts\n useEffect(() => installConsentWindowApi(), []);\n\n // During render, not an effect: the first-party pixel mounts earlier and its\n // injection effect must see this.\n const platform = findConsentPlatform(scripts);\n if (platform) {\n enableConsentDelegation(platform);\n }\n\n // The loader renders LAST: children's effects run in order, so every blocked\n // tag is in the DOM when the loader's startup scan runs \u2014 generic platforms\n // need no Cookiebot-style re-scan API.\n return (\n <>\n {scripts\n .filter((entry) => !isConsentPlatformEntry(entry))\n .map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n {platform &&\n !platform.isCookiebot &&\n OPTIONAL_CONSENT_CATEGORIES.map((category) => (\n <InjectedScript\n key={category}\n baseId={`consent-platform:${platform.id}:grant:${category}`}\n descriptors={[buildGrantMarker(category)]}\n requiredConsent={[category]}\n />\n ))}\n {platform && (\n <InjectedScript\n baseId={`consent-platform:${platform.id}`}\n descriptors={platform.loader}\n />\n )}\n </>\n );\n}\n"],
5
+ "mappings": ";AAiKI,SAiCA,UAjCA,KAiCA,YAjCA;AAzJJ,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB,kBAAkB;AAC7C,SAAS,+BAA+B;AACxC,SAAS,gBAAgB,wBAAwB;AACjD,SAAS,iBAAiB,yBAAyB;AACnD,SAAS,+BAA+B;AAMxC,SAAS,SAAS,OAAiC;AACjD,MAAI,MAAM,SAAS,UAAU;AAC3B,WAAO,UAAU,MAAM,EAAE;AAAA,EAC3B;AACA,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,WAAW,MAAM,EAAE;AAAA,EAC5B;AACA,MAAI,MAAM,SAAS,mBAAmB;AACpC,WAAO,oBAAoB,MAAM,EAAE;AAAA,EACrC;AACA,SAAO,GAAG,MAAM,IAAI,IAAI,MAAM,UAAU;AAC1C;AAEA,SAAS,mBAAmB,OAA4C;AACtE,MAAI,MAAM,SAAS,YAAY,MAAM,SAAS,WAAW;AACvD,WAAO,MAAM;AAAA,EACf;AAEA,MAAI,MAAM,SAAS,qBAAqB,MAAM,SAAS,aAAa;AAClE,WAAO,CAAC;AAAA,EACV;AACA,SAAO,MAAM,mBAAmB,kBAAkB,MAAM,IAAI;AAC9D;AAQA,SAAS,eAAe,OAAgD;AACtE,MAAI,MAAM,SAAS,UAAU;AAC3B,QAAI,MAAM,KAAK;AACb,aAAO,CAAC,EAAE,MAAM,YAAY,KAAK,MAAM,IAAI,CAAC;AAAA,IAC9C;AACA,QAAI,MAAM,MAAM;AACd,aAAO,CAAC,EAAE,MAAM,UAAU,MAAM,MAAM,KAAK,CAAC;AAAA,IAC9C;AACA,WAAO,CAAC;AAAA,EACV;AACA,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,CAAC,EAAE,MAAM,UAAU,MAAM,MAAM,MAAM,QAAQ,MAAM,OAAO,CAAC;AAAA,EACpE;AAGA,MAAI,MAAM,SAAS,qBAAqB,MAAM,SAAS,aAAa;AAClE,WAAO,CAAC;AAAA,EACV;AACA,SAAO,gBAAgB,EAAE,MAAM,MAAM,MAAM,YAAY,MAAM,WAAW,CAAC;AAC3E;AAYO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,kBAAkB,CAAC;AACrB,GAKG;AAED,QAAM,eAAe,GAAG,MAAM,IAAI,KAAK,UAAU,WAAW,CAAC,IAAI,gBAAgB,KAAK,GAAG,CAAC;AAG1F,YAAU,MAAM;AACd,QAAI,OAAO,aAAa,aAAa;AACnC;AAAA,IACF;AAGA,UAAM,WAAW,sBAAsB;AACvC,UAAM,qBAAqB,WACvB,sBAAsB,UAAU,eAAe,IAC/C,CAAC;AACL,UAAM,oBAAoB,UAAU;AACpC,UAAM,eAAe,wBAAwB;AAAA,MAC3C;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AAGD,QAAI,qBAAqB,aAAa,SAAS,GAAG;AAChD,6BAAuB;AAAA,IACzB;AAEA,WAAO,MAAM;AAIX,iBAAW,QAAQ,cAAc;AAC/B,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;AAEA,SAAS,cAAc,EAAE,MAAM,GAAgC;AAC7D,QAAM,UAAU,WAAW;AAC3B,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,QAAM,KAAK,SAAS,KAAK;AACzB,QAAM,aAAa,gBAAgB,KAAK,GAAG;AAG3C,YAAU,MAAM;AACd,mBAAe,EAAE,IAAI,MAAM,MAAM,MAAM,gBAAgB,CAAC;AACxD,WAAO,MAAM,iBAAiB,EAAE;AAAA,EAClC,GAAG,CAAC,IAAI,MAAM,MAAM,UAAU,CAAC;AAI/B,MACE,CAAC,sBAAsB,KACvB,CAAC,iBAAiB,EAAE,OAAO,SAAS,gBAAgB,CAAC,GACrD;AACA,WAAO;AAAA,EACT;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ;AAAA,MACR,aAAa,eAAe,KAAK;AAAA,MACjC;AAAA;AAAA,EACF;AAEJ;AAYO,SAAS,aAAa,EAAE,QAAQ,GAAoC;AAEzE,YAAU,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAI7C,QAAM,WAAW,oBAAoB,OAAO;AAC5C,MAAI,UAAU;AACZ,4BAAwB,QAAQ;AAAA,EAClC;AAKA,SACE,iCACG;AAAA,YACE,OAAO,CAAC,UAAU,CAAC,uBAAuB,KAAK,CAAC,EAChD,IAAI,CAAC,UACJ,oBAAC,iBAAoC,SAAjB,SAAS,KAAK,CAAiB,CACpD;AAAA,IACF,YACC,CAAC,SAAS,eACV,4BAA4B,IAAI,CAAC,aAC/B;AAAA,MAAC;AAAA;AAAA,QAEC,QAAQ,oBAAoB,SAAS,EAAE,UAAU,QAAQ;AAAA,QACzD,aAAa,CAAC,iBAAiB,QAAQ,CAAC;AAAA,QACxC,iBAAiB,CAAC,QAAQ;AAAA;AAAA,MAHrB;AAAA,IAIP,CACD;AAAA,IACF,YACC;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,oBAAoB,SAAS,EAAE;AAAA,QACvC,aAAa,SAAS;AAAA;AAAA,IACxB;AAAA,KAEJ;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import type { ConsentCategory, ReploScriptType, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
2
- type ProviderType = Exclude<ReploScriptType, "custom" | "snippet">;
2
+ type ProviderType = Exclude<ReploScriptType, "custom" | "snippet" | "consentPlatform" | "Cookiebot">;
3
3
  /**
4
4
  * Snippet providers don't synthesize tags (the user pastes the provider-issued
5
5
  * snippet as the entry's `body`), but they still get a first-class default
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../consent/script-snippets.ts"],
4
- "sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\ntype ProviderType = Exclude<ReploScriptType, \"custom\" | \"snippet\">;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a),e.trackPageViewInitial(window.location.href);})()`,\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\n// Cookiebot's loader is built by its consent-platform preset, not here.\ntype ProviderType = Exclude<\n ReploScriptType,\n \"custom\" | \"snippet\" | \"consentPlatform\" | \"Cookiebot\"\n>;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a),e.trackPageViewInitial(window.location.href);})()`,\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "snippet" | "custom";
1
+ export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
2
2
  export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
3
3
  export type ConsentMode = "off" | "simple" | "per-category";
4
4
  declare module "react" {
@@ -17,7 +17,7 @@ export type ScriptTagDescriptor = {
17
17
  body: string;
18
18
  };
19
19
  export type ReploScriptEntry = {
20
- type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam";
20
+ type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Cookiebot";
21
21
  identifier: string;
22
22
  requiredConsent?: ConsentCategory[];
23
23
  } | {
@@ -31,4 +31,15 @@ export type ReploScriptEntry = {
31
31
  requiredConsent: ConsentCategory[];
32
32
  src?: string;
33
33
  body?: string;
34
+ } | {
35
+ type: "consentPlatform";
36
+ id: string;
37
+ src: string;
38
+ attributes?: {
39
+ [x: string]: string;
40
+ };
41
+ blockingAttribute: {
42
+ name: string;
43
+ values: Partial<Record<ConsentCategory, string>>;
44
+ };
34
45
  };
@@ -18,7 +18,10 @@ export interface ReploCustomerPrivacyApi {
18
18
  marketingAllowed(): boolean;
19
19
  preferencesProcessingAllowed(): boolean;
20
20
  saleOfDataAllowed(): boolean;
21
- /** Whether the consent banner should still be shown (mode on + undecided). */
21
+ /**
22
+ * Whether Replo's consent banner should still be shown (mode on + undecided).
23
+ * Always false when an external CMP owns consent and shows its own banner.
24
+ */
22
25
  shouldShowBanner(): boolean;
23
26
  /** Update a subset of categories; runs the optional callback once persisted. */
24
27
  setTrackingConsent(consent: Partial<Record<ToggleableCategory, boolean>>, callback?: () => void): void;
@@ -30,6 +30,9 @@ const api = {
30
30
  saleOfDataAllowed: () => isAllowed("sale_of_data"),
31
31
  shouldShowBanner: () => {
32
32
  const state = consentStore.getSnapshot();
33
+ if (state.cmp === "cookiebot" || state.cmp === "external") {
34
+ return false;
35
+ }
33
36
  return state.mode !== "off" && state.decidedAt === null;
34
37
  },
35
38
  setTrackingConsent: (consent, callback) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../consent/window-api.ts"],
4
- "sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory } from \"schemas/generated/consent\";\n\nimport { consentStore, isConsentAllowed } from \"./consent-store\";\n\n/**\n * Global, framework-agnostic consent API exposed on `window.Replo.customerPrivacy`,\n * mirroring Shopify's Customer Privacy API surface so on-page (third-party)\n * scripts can query consent and react to changes without importing canopy.\n *\n * It deliberately lives under our own `window.Replo` namespace rather than\n * `window.Shopify` to avoid colliding with Shopify's real API on Shopify-backed\n * sites.\n */\n\ntype ToggleableCategory = Exclude<ConsentCategory, \"necessary\">;\n\n/** Shopify-style tri-state: declared-allowed, declared-denied, or undeclared. */\ntype ConsentSignal = \"yes\" | \"no\" | \"\";\n\nexport interface ReploCustomerPrivacyApi {\n /** Per-category declaration, or \"\" when the visitor has not decided. */\n currentVisitorConsent(): Record<ToggleableCategory, ConsentSignal>;\n analyticsProcessingAllowed(): boolean;\n marketingAllowed(): boolean;\n preferencesProcessingAllowed(): boolean;\n saleOfDataAllowed(): boolean;\n /** Whether the consent banner should still be shown (mode on + undecided). */\n shouldShowBanner(): boolean;\n /** Update a subset of categories; runs the optional callback once persisted. */\n setTrackingConsent(\n consent: Partial<Record<ToggleableCategory, boolean>>,\n callback?: () => void,\n ): void;\n}\n\ndeclare global {\n interface Window {\n Replo?: { customerPrivacy?: ReploCustomerPrivacyApi };\n }\n}\n\n/** DOM event dispatched on `document` whenever consent changes. */\nexport const CONSENT_CHANGE_EVENT = \"visitorConsentCollected\";\n\nfunction isAllowed(category: ConsentCategory): boolean {\n return isConsentAllowed({\n state: consentStore.getSnapshot(),\n requiredConsent: [category],\n });\n}\n\nfunction currentVisitorConsent(): Record<ToggleableCategory, ConsentSignal> {\n const state = consentStore.getSnapshot();\n const signal = (category: ToggleableCategory): ConsentSignal => {\n if (state.decidedAt === null) {\n return \"\";\n }\n return state.categories[category] ? \"yes\" : \"no\";\n };\n return {\n analytics: signal(\"analytics\"),\n marketing: signal(\"marketing\"),\n preferences: signal(\"preferences\"),\n sale_of_data: signal(\"sale_of_data\"),\n };\n}\n\nconst api: ReploCustomerPrivacyApi = {\n currentVisitorConsent,\n analyticsProcessingAllowed: () => isAllowed(\"analytics\"),\n marketingAllowed: () => isAllowed(\"marketing\"),\n preferencesProcessingAllowed: () => isAllowed(\"preferences\"),\n saleOfDataAllowed: () => isAllowed(\"sale_of_data\"),\n shouldShowBanner: () => {\n const state = consentStore.getSnapshot();\n return state.mode !== \"off\" && state.decidedAt === null;\n },\n setTrackingConsent: (consent, callback) => {\n consentStore.updateCategories({ next: consent });\n callback?.();\n },\n};\n\nfunction consentChangeDetail() {\n return {\n analyticsAllowed: isAllowed(\"analytics\"),\n marketingAllowed: isAllowed(\"marketing\"),\n preferencesAllowed: isAllowed(\"preferences\"),\n saleOfDataAllowed: isAllowed(\"sale_of_data\"),\n };\n}\n\nlet teardown: (() => void) | null = null;\n\n/**\n * Installs `window.Replo.customerPrivacy` and dispatches `visitorConsentCollected`\n * on `document` whenever consent changes. Idempotent; returns a teardown that\n * removes the change subscription. Safe to call on the server (no-op).\n */\nexport function installConsentWindowApi(): () => void {\n if (typeof window === \"undefined\") {\n return () => {\n // no-op on the server\n };\n }\n if (teardown) {\n return teardown;\n }\n window.Replo = { ...window.Replo, customerPrivacy: api };\n const unsubscribe = consentStore.subscribe(() => {\n document.dispatchEvent(\n new CustomEvent(CONSENT_CHANGE_EVENT, { detail: consentChangeDetail() }),\n );\n });\n teardown = () => {\n unsubscribe();\n teardown = null;\n };\n return teardown;\n}\n"],
5
- "mappings": ";AAIA,SAAS,cAAc,wBAAwB;AAwCxC,MAAM,uBAAuB;AAEpC,SAAS,UAAU,UAAoC;AACrD,SAAO,iBAAiB;AAAA,IACtB,OAAO,aAAa,YAAY;AAAA,IAChC,iBAAiB,CAAC,QAAQ;AAAA,EAC5B,CAAC;AACH;AAEA,SAAS,wBAAmE;AAC1E,QAAM,QAAQ,aAAa,YAAY;AACvC,QAAM,SAAS,CAAC,aAAgD;AAC9D,QAAI,MAAM,cAAc,MAAM;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,MAAM,WAAW,QAAQ,IAAI,QAAQ;AAAA,EAC9C;AACA,SAAO;AAAA,IACL,WAAW,OAAO,WAAW;AAAA,IAC7B,WAAW,OAAO,WAAW;AAAA,IAC7B,aAAa,OAAO,aAAa;AAAA,IACjC,cAAc,OAAO,cAAc;AAAA,EACrC;AACF;AAEA,MAAM,MAA+B;AAAA,EACnC;AAAA,EACA,4BAA4B,MAAM,UAAU,WAAW;AAAA,EACvD,kBAAkB,MAAM,UAAU,WAAW;AAAA,EAC7C,8BAA8B,MAAM,UAAU,aAAa;AAAA,EAC3D,mBAAmB,MAAM,UAAU,cAAc;AAAA,EACjD,kBAAkB,MAAM;AACtB,UAAM,QAAQ,aAAa,YAAY;AACvC,WAAO,MAAM,SAAS,SAAS,MAAM,cAAc;AAAA,EACrD;AAAA,EACA,oBAAoB,CAAC,SAAS,aAAa;AACzC,iBAAa,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC/C,eAAW;AAAA,EACb;AACF;AAEA,SAAS,sBAAsB;AAC7B,SAAO;AAAA,IACL,kBAAkB,UAAU,WAAW;AAAA,IACvC,kBAAkB,UAAU,WAAW;AAAA,IACvC,oBAAoB,UAAU,aAAa;AAAA,IAC3C,mBAAmB,UAAU,cAAc;AAAA,EAC7C;AACF;AAEA,IAAI,WAAgC;AAO7B,SAAS,0BAAsC;AACpD,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,MAAM;AAAA,IAEb;AAAA,EACF;AACA,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,EAAE,GAAG,OAAO,OAAO,iBAAiB,IAAI;AACvD,QAAM,cAAc,aAAa,UAAU,MAAM;AAC/C,aAAS;AAAA,MACP,IAAI,YAAY,sBAAsB,EAAE,QAAQ,oBAAoB,EAAE,CAAC;AAAA,IACzE;AAAA,EACF,CAAC;AACD,aAAW,MAAM;AACf,gBAAY;AACZ,eAAW;AAAA,EACb;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory } from \"schemas/generated/consent\";\n\nimport { consentStore, isConsentAllowed } from \"./consent-store\";\n\n/**\n * Global, framework-agnostic consent API exposed on `window.Replo.customerPrivacy`,\n * mirroring Shopify's Customer Privacy API surface so on-page (third-party)\n * scripts can query consent and react to changes without importing canopy.\n *\n * It deliberately lives under our own `window.Replo` namespace rather than\n * `window.Shopify` to avoid colliding with Shopify's real API on Shopify-backed\n * sites.\n */\n\ntype ToggleableCategory = Exclude<ConsentCategory, \"necessary\">;\n\n/** Shopify-style tri-state: declared-allowed, declared-denied, or undeclared. */\ntype ConsentSignal = \"yes\" | \"no\" | \"\";\n\nexport interface ReploCustomerPrivacyApi {\n /** Per-category declaration, or \"\" when the visitor has not decided. */\n currentVisitorConsent(): Record<ToggleableCategory, ConsentSignal>;\n analyticsProcessingAllowed(): boolean;\n marketingAllowed(): boolean;\n preferencesProcessingAllowed(): boolean;\n saleOfDataAllowed(): boolean;\n /**\n * Whether Replo's consent banner should still be shown (mode on + undecided).\n * Always false when an external CMP owns consent and shows its own banner.\n */\n shouldShowBanner(): boolean;\n /** Update a subset of categories; runs the optional callback once persisted. */\n setTrackingConsent(\n consent: Partial<Record<ToggleableCategory, boolean>>,\n callback?: () => void,\n ): void;\n}\n\ndeclare global {\n interface Window {\n Replo?: { customerPrivacy?: ReploCustomerPrivacyApi };\n }\n}\n\n/** DOM event dispatched on `document` whenever consent changes. */\nexport const CONSENT_CHANGE_EVENT = \"visitorConsentCollected\";\n\nfunction isAllowed(category: ConsentCategory): boolean {\n return isConsentAllowed({\n state: consentStore.getSnapshot(),\n requiredConsent: [category],\n });\n}\n\nfunction currentVisitorConsent(): Record<ToggleableCategory, ConsentSignal> {\n const state = consentStore.getSnapshot();\n const signal = (category: ToggleableCategory): ConsentSignal => {\n if (state.decidedAt === null) {\n return \"\";\n }\n return state.categories[category] ? \"yes\" : \"no\";\n };\n return {\n analytics: signal(\"analytics\"),\n marketing: signal(\"marketing\"),\n preferences: signal(\"preferences\"),\n sale_of_data: signal(\"sale_of_data\"),\n };\n}\n\nconst api: ReploCustomerPrivacyApi = {\n currentVisitorConsent,\n analyticsProcessingAllowed: () => isAllowed(\"analytics\"),\n marketingAllowed: () => isAllowed(\"marketing\"),\n preferencesProcessingAllowed: () => isAllowed(\"preferences\"),\n saleOfDataAllowed: () => isAllowed(\"sale_of_data\"),\n shouldShowBanner: () => {\n const state = consentStore.getSnapshot();\n // A delegated platform renders its own banner; showing Replo's too would\n // ask the visitor twice.\n if (state.cmp === \"cookiebot\" || state.cmp === \"external\") {\n return false;\n }\n return state.mode !== \"off\" && state.decidedAt === null;\n },\n setTrackingConsent: (consent, callback) => {\n consentStore.updateCategories({ next: consent });\n callback?.();\n },\n};\n\nfunction consentChangeDetail() {\n return {\n analyticsAllowed: isAllowed(\"analytics\"),\n marketingAllowed: isAllowed(\"marketing\"),\n preferencesAllowed: isAllowed(\"preferences\"),\n saleOfDataAllowed: isAllowed(\"sale_of_data\"),\n };\n}\n\nlet teardown: (() => void) | null = null;\n\n/**\n * Installs `window.Replo.customerPrivacy` and dispatches `visitorConsentCollected`\n * on `document` whenever consent changes. Idempotent; returns a teardown that\n * removes the change subscription. Safe to call on the server (no-op).\n */\nexport function installConsentWindowApi(): () => void {\n if (typeof window === \"undefined\") {\n return () => {\n // no-op on the server\n };\n }\n if (teardown) {\n return teardown;\n }\n window.Replo = { ...window.Replo, customerPrivacy: api };\n const unsubscribe = consentStore.subscribe(() => {\n document.dispatchEvent(\n new CustomEvent(CONSENT_CHANGE_EVENT, { detail: consentChangeDetail() }),\n );\n });\n teardown = () => {\n unsubscribe();\n teardown = null;\n };\n return teardown;\n}\n"],
5
+ "mappings": ";AAIA,SAAS,cAAc,wBAAwB;AA2CxC,MAAM,uBAAuB;AAEpC,SAAS,UAAU,UAAoC;AACrD,SAAO,iBAAiB;AAAA,IACtB,OAAO,aAAa,YAAY;AAAA,IAChC,iBAAiB,CAAC,QAAQ;AAAA,EAC5B,CAAC;AACH;AAEA,SAAS,wBAAmE;AAC1E,QAAM,QAAQ,aAAa,YAAY;AACvC,QAAM,SAAS,CAAC,aAAgD;AAC9D,QAAI,MAAM,cAAc,MAAM;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,MAAM,WAAW,QAAQ,IAAI,QAAQ;AAAA,EAC9C;AACA,SAAO;AAAA,IACL,WAAW,OAAO,WAAW;AAAA,IAC7B,WAAW,OAAO,WAAW;AAAA,IAC7B,aAAa,OAAO,aAAa;AAAA,IACjC,cAAc,OAAO,cAAc;AAAA,EACrC;AACF;AAEA,MAAM,MAA+B;AAAA,EACnC;AAAA,EACA,4BAA4B,MAAM,UAAU,WAAW;AAAA,EACvD,kBAAkB,MAAM,UAAU,WAAW;AAAA,EAC7C,8BAA8B,MAAM,UAAU,aAAa;AAAA,EAC3D,mBAAmB,MAAM,UAAU,cAAc;AAAA,EACjD,kBAAkB,MAAM;AACtB,UAAM,QAAQ,aAAa,YAAY;AAGvC,QAAI,MAAM,QAAQ,eAAe,MAAM,QAAQ,YAAY;AACzD,aAAO;AAAA,IACT;AACA,WAAO,MAAM,SAAS,SAAS,MAAM,cAAc;AAAA,EACrD;AAAA,EACA,oBAAoB,CAAC,SAAS,aAAa;AACzC,iBAAa,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC/C,eAAW;AAAA,EACb;AACF;AAEA,SAAS,sBAAsB;AAC7B,SAAO;AAAA,IACL,kBAAkB,UAAU,WAAW;AAAA,IACvC,kBAAkB,UAAU,WAAW;AAAA,IACvC,oBAAoB,UAAU,aAAa;AAAA,IAC3C,mBAAmB,UAAU,cAAc;AAAA,EAC7C;AACF;AAEA,IAAI,WAAgC;AAO7B,SAAS,0BAAsC;AACpD,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,MAAM;AAAA,IAEb;AAAA,EACF;AACA,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,EAAE,GAAG,OAAO,OAAO,iBAAiB,IAAI;AACvD,QAAM,cAAc,aAAa,UAAU,MAAM;AAC/C,aAAS;AAAA,MACP,IAAI,YAAY,sBAAsB,EAAE,QAAQ,oBAAoB,EAAE,CAAC;AAAA,IACzE;AAAA,EACF,CAAC;AACD,aAAW,MAAM;AACf,gBAAY;AACZ,eAAW;AAAA,EACb;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "branch": "HEAD",
5
- "commit": "53885fcc411325244dae08b8ba0697eea8e8b1b5",
6
- "builtAt": "2026-08-19T22:25:34.675Z"
5
+ "commit": "5076021b33cfdcd5bddd4ee545268e6d34896ee2",
6
+ "builtAt": "2026-08-20T20:41:16.929Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "0.6.1",
6
+ "version": "0.8.0",
7
7
  "branch": "HEAD",
8
- "commit": "53885fcc411325244dae08b8ba0697eea8e8b1b5",
9
- "builtAt": "2026-08-19T22:25:34.675Z"
8
+ "commit": "5076021b33cfdcd5bddd4ee545268e6d34896ee2",
9
+ "builtAt": "2026-08-20T20:41:16.929Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -209,6 +209,14 @@
209
209
  "types": "./routing/rules.d.ts",
210
210
  "default": "./routing/rules.js"
211
211
  },
212
+ "./routing/locale": {
213
+ "types": "./routing/locale.d.ts",
214
+ "default": "./routing/locale.js"
215
+ },
216
+ "./routing/geo": {
217
+ "types": "./routing/geo.d.ts",
218
+ "default": "./routing/geo.js"
219
+ },
212
220
  "./_vendor/schemas/loaderKeys": {
213
221
  "types": "./_vendor/schemas/loaderKeys.d.ts",
214
222
  "default": "./_vendor/schemas/loaderKeys.mjs"
@@ -216,6 +224,8 @@
216
224
  "./package.json": "./package.json"
217
225
  },
218
226
  "dependencies": {
227
+ "@formatjs/intl-localematcher": "^0.8.13",
228
+ "negotiator": "^1.0.0",
219
229
  "tiny-jsonc": "^1.0.2",
220
230
  "uuid": "^11.1.0",
221
231
  "zod": "^4.3.6"
@@ -1,7 +1,9 @@
1
1
  import type { NextRequest } from "next/server";
2
+ import type { LocaleRoutingConfig } from "../_vendor/schemas/routing/locale";
2
3
  import type { RedirectRule } from "../_vendor/schemas/routing/rules";
3
4
  import { NextResponse } from "next/server";
4
- export declare function evaluateRouting({ request, rules, }: {
5
+ export declare function evaluateRouting({ request, rules, localeRouting, }: {
5
6
  request: NextRequest;
6
7
  rules: readonly RedirectRule[];
8
+ localeRouting?: LocaleRoutingConfig;
7
9
  }): NextResponse;
@@ -1,25 +1,66 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { REPLO_LOCALE_COOKIE } from "../_vendor/schemas/routing/locale.mjs";
3
+ import {
4
+ getLocaleFromPath,
5
+ negotiateLocale,
6
+ resolveLocaleScope,
7
+ withLocalePrefix
8
+ } from "../_vendor/schemas/routing/localeNegotiation.mjs";
2
9
  import { applyParams, matchSource } from "../_vendor/schemas/routing/match.mjs";
10
+ import { ipCountry } from "./geo";
3
11
  function evaluateRouting({
4
12
  request,
5
- rules
13
+ rules,
14
+ localeRouting
6
15
  }) {
16
+ const localeContext = localeRouting ? getLocaleContext({ request, config: localeRouting }) : null;
7
17
  for (const rule of rules) {
8
- const response = evaluateRedirect({ request, rule });
18
+ const response = evaluateRedirect({ request, rule, localeContext });
9
19
  if (response) {
10
20
  return response;
11
21
  }
12
22
  }
13
- return NextResponse.next();
23
+ return evaluateLocalePhase({ request, localeContext });
24
+ }
25
+ function getLocaleContext({
26
+ request,
27
+ config
28
+ }) {
29
+ const scope = resolveLocaleScope({
30
+ host: request.headers.get("host"),
31
+ config
32
+ });
33
+ const pathLocale = getLocaleFromPath({
34
+ pathname: request.nextUrl.pathname,
35
+ locales: config.locales
36
+ });
37
+ const negotiated = negotiateLocale({
38
+ scope,
39
+ detection: config.detection,
40
+ cookieLocale: request.cookies.get(REPLO_LOCALE_COOKIE)?.value ?? null,
41
+ countryCode: ipCountry(request),
42
+ acceptLanguageHeader: request.headers.get("accept-language")
43
+ });
44
+ return {
45
+ config,
46
+ scope,
47
+ urlMode: scope.locales.length === 1 ? "hidden" : config.urlMode,
48
+ pathLocale,
49
+ negotiated
50
+ };
14
51
  }
15
52
  function evaluateRedirect({
16
53
  request,
17
- rule
54
+ rule,
55
+ localeContext
18
56
  }) {
19
57
  const params = matchSource({
20
58
  source: rule.source,
21
59
  pathname: request.nextUrl.pathname
22
- });
60
+ }) ?? (localeContext?.pathLocale ? matchSource({
61
+ source: rule.source,
62
+ pathname: localeContext.pathLocale.logicalPath
63
+ }) : null);
23
64
  if (!params) {
24
65
  return null;
25
66
  }
@@ -30,11 +71,114 @@ function evaluateRedirect({
30
71
  if (url.origin === request.nextUrl.origin && !url.search) {
31
72
  url.search = request.nextUrl.search;
32
73
  }
74
+ if (localeContext && url.origin === request.nextUrl.origin) {
75
+ applyLocaleToDestination({ url, localeContext });
76
+ }
33
77
  const redirectsToCurrentRequest = url.origin === request.nextUrl.origin && normalizePath(url.pathname) === normalizePath(request.nextUrl.pathname) && url.search === request.nextUrl.search;
34
78
  if (redirectsToCurrentRequest) {
35
79
  return NextResponse.next();
36
80
  }
37
- return NextResponse.redirect(url, rule.status);
81
+ const response = NextResponse.redirect(url, rule.status);
82
+ if (localeContext?.pathLocale && localeContext.urlMode === "hidden" && url.origin === request.nextUrl.origin && localeContext.scope.locales.includes(localeContext.pathLocale.locale)) {
83
+ setLocaleCookie({ response, locale: localeContext.pathLocale.locale });
84
+ }
85
+ return response;
86
+ }
87
+ function applyLocaleToDestination({
88
+ url,
89
+ localeContext
90
+ }) {
91
+ if (localeContext.urlMode !== "prefixed") {
92
+ return;
93
+ }
94
+ if (isLocaleExemptPath(url.pathname)) {
95
+ return;
96
+ }
97
+ const destinationLocale = getLocaleFromPath({
98
+ pathname: url.pathname,
99
+ locales: localeContext.config.locales
100
+ });
101
+ if (destinationLocale) {
102
+ if (destinationLocale.pathSegment !== destinationLocale.locale) {
103
+ url.pathname = withLocalePrefix({
104
+ locale: destinationLocale.locale,
105
+ path: destinationLocale.logicalPath
106
+ });
107
+ }
108
+ return;
109
+ }
110
+ url.pathname = withLocalePrefix({
111
+ locale: localeContext.pathLocale?.locale ?? localeContext.negotiated.locale,
112
+ path: url.pathname
113
+ });
114
+ }
115
+ function evaluateLocalePhase({
116
+ request,
117
+ localeContext
118
+ }) {
119
+ if (!localeContext) {
120
+ return NextResponse.next();
121
+ }
122
+ const { pathname } = request.nextUrl;
123
+ if (isLocaleExemptPath(pathname)) {
124
+ return NextResponse.next();
125
+ }
126
+ const { urlMode, pathLocale, negotiated, scope } = localeContext;
127
+ if (urlMode === "prefixed") {
128
+ if (!pathLocale) {
129
+ const url2 = request.nextUrl.clone();
130
+ url2.pathname = withLocalePrefix({
131
+ locale: negotiated.locale,
132
+ path: pathname
133
+ });
134
+ return NextResponse.redirect(url2, 307);
135
+ }
136
+ if (pathLocale.pathSegment !== pathLocale.locale) {
137
+ const url2 = request.nextUrl.clone();
138
+ url2.pathname = withLocalePrefix({
139
+ locale: pathLocale.locale,
140
+ path: pathLocale.logicalPath
141
+ });
142
+ return NextResponse.redirect(url2, 308);
143
+ }
144
+ const response = NextResponse.next();
145
+ if (pathLocale.locale !== negotiated.locale && request.cookies.get(REPLO_LOCALE_COOKIE)?.value !== pathLocale.locale && scope.locales.includes(pathLocale.locale)) {
146
+ setLocaleCookie({ response, locale: pathLocale.locale });
147
+ }
148
+ return response;
149
+ }
150
+ if (pathLocale) {
151
+ const url2 = request.nextUrl.clone();
152
+ url2.pathname = pathLocale.logicalPath;
153
+ const response = NextResponse.redirect(url2, 307);
154
+ if (scope.locales.includes(pathLocale.locale)) {
155
+ setLocaleCookie({ response, locale: pathLocale.locale });
156
+ }
157
+ return response;
158
+ }
159
+ const url = request.nextUrl.clone();
160
+ url.pathname = withLocalePrefix({
161
+ locale: negotiated.locale,
162
+ path: pathname
163
+ });
164
+ return NextResponse.rewrite(url);
165
+ }
166
+ function isLocaleExemptPath(pathname) {
167
+ if (pathname === "/api" || pathname.startsWith("/api/")) {
168
+ return true;
169
+ }
170
+ const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
171
+ return lastSegment.includes(".");
172
+ }
173
+ function setLocaleCookie({
174
+ response,
175
+ locale
176
+ }) {
177
+ response.cookies.set(REPLO_LOCALE_COOKIE, locale, {
178
+ path: "/",
179
+ maxAge: 31536e3,
180
+ sameSite: "lax"
181
+ });
38
182
  }
39
183
  function normalizePath(pathname) {
40
184
  const trimmed = pathname.replace(/\/+$/, "");