@reblu/site-next 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/analytics.tsx
31
+ var analytics_exports = {};
32
+ __export(analytics_exports, {
33
+ AnalyticsScripts: () => AnalyticsScripts,
34
+ CLARITY_PROJECT_ID_PATTERN: () => CLARITY_PROJECT_ID_PATTERN,
35
+ GA_MEASUREMENT_ID_PATTERN: () => GA_MEASUREMENT_ID_PATTERN,
36
+ analyticsScriptLiteral: () => analyticsScriptLiteral,
37
+ clarityInitScript: () => clarityInitScript,
38
+ gaInitScript: () => gaInitScript,
39
+ isValidClarityProjectId: () => isValidClarityProjectId,
40
+ isValidGaMeasurementId: () => isValidGaMeasurementId
41
+ });
42
+ module.exports = __toCommonJS(analytics_exports);
43
+ var import_script = __toESM(require("next/script"), 1);
44
+
45
+ // src/json-ld.tsx
46
+ var import_jsx_runtime = require("react/jsx-runtime");
47
+ var SCRIPT_BOUNDARY_UNSAFE = /[<>&\u2028\u2029]/g;
48
+ var ESCAPES = {
49
+ "<": "\\u003c",
50
+ ">": "\\u003e",
51
+ "&": "\\u0026",
52
+ "\u2028": "\\u2028",
53
+ "\u2029": "\\u2029"
54
+ };
55
+ function jsonLdScriptContent(value) {
56
+ const json = JSON.stringify(value) ?? "null";
57
+ return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char]);
58
+ }
59
+
60
+ // src/analytics.tsx
61
+ var import_jsx_runtime2 = require("react/jsx-runtime");
62
+ var GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/;
63
+ var CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/;
64
+ function isValidGaMeasurementId(value) {
65
+ return typeof value === "string" && GA_MEASUREMENT_ID_PATTERN.test(value);
66
+ }
67
+ function isValidClarityProjectId(value) {
68
+ return typeof value === "string" && CLARITY_PROJECT_ID_PATTERN.test(value);
69
+ }
70
+ function analyticsScriptLiteral(id) {
71
+ return jsonLdScriptContent(id);
72
+ }
73
+ function gaInitScript(gaId) {
74
+ return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`;
75
+ }
76
+ function clarityInitScript(clarityId) {
77
+ return `(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",${analyticsScriptLiteral(clarityId)});`;
78
+ }
79
+ function AnalyticsScripts({ gaId, clarityId }) {
80
+ const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null;
81
+ const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null;
82
+ if (!safeGaId && !safeClarityId) return null;
83
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
84
+ safeGaId && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
85
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
86
+ import_script.default,
87
+ {
88
+ src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`,
89
+ strategy: "afterInteractive"
90
+ }
91
+ ),
92
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
93
+ import_script.default,
94
+ {
95
+ id: "ga-init",
96
+ strategy: "afterInteractive",
97
+ dangerouslySetInnerHTML: { __html: gaInitScript(safeGaId) }
98
+ }
99
+ )
100
+ ] }),
101
+ safeClarityId && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
102
+ import_script.default,
103
+ {
104
+ id: "clarity-init",
105
+ strategy: "afterInteractive",
106
+ dangerouslySetInnerHTML: { __html: clarityInitScript(safeClarityId) }
107
+ }
108
+ )
109
+ ] });
110
+ }
111
+ // Annotate the CommonJS export names for ESM import in node:
112
+ 0 && (module.exports = {
113
+ AnalyticsScripts,
114
+ CLARITY_PROJECT_ID_PATTERN,
115
+ GA_MEASUREMENT_ID_PATTERN,
116
+ analyticsScriptLiteral,
117
+ clarityInitScript,
118
+ gaInitScript,
119
+ isValidClarityProjectId,
120
+ isValidGaMeasurementId
121
+ });
122
+ //# sourceMappingURL=analytics.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analytics.tsx","../src/json-ld.tsx"],"sourcesContent":["import Script from 'next/script'\n\nimport { jsonLdScriptContent } from './json-ld'\n\n/**\n * `<AnalyticsScripts>` — safe rendering of tenant-configured analytics tags (REB-617).\n *\n * THE HOLE THIS CLOSES. Every SITE built on this SDK interpolated the tenant's\n * analytics ids straight into the body of an executing `<script>`:\n *\n * gtag('config','${analytics.gaId}');\n * …\"clarity\",\"script\",\"${analytics.clarityId}\");\n *\n * No validation, no escaping. A `gaId` of `x');alert(document.cookie);//` closes\n * the string literal, closes the call, comments out the rest, and runs arbitrary\n * JS on the TENANT'S OWN ORIGIN — on every page including checkout, same origin\n * as the portal session cookie. For a medical tenant that is patient-session\n * territory. It is the same trust boundary as the nexus-authored HTML that\n * REB-613 closed on the sink next door, except this one reached the JS\n * interpreter with no sanitiser anywhere in the path.\n *\n * TWO INDEPENDENT LAYERS, because either alone is one refactor away from failing:\n *\n * 1. VALIDATION — an id that does not match its vendor-documented shape is\n * dropped and NO script renders. Analytics silently does not load; the site\n * keeps working. Failing closed matters more than failing loud here: the\n * value is cosmetic, the sink is not.\n * 2. ESCAPING — `analyticsScriptLiteral` emits the id as a JSON string literal\n * with the `<script>` boundary neutralised, so even a value that skipped\n * layer 1 (a future field with a looser shape, a caller that reaches for the\n * builders directly) is inert DATA rather than code.\n *\n * WHY VALIDATION ALONE IS NOT ENOUGH, and escaping alone is not either: layer 1\n * is a policy that a product decision can widen (someone adds `GT-` ids, or a\n * Meta Pixel field whose shape is numeric-but-unbounded), and layer 2 is a\n * mechanism that cannot express \"this is not a usable analytics id\". Keeping both\n * means widening the policy never reopens the sink.\n *\n * This lives in the SDK, not in `apps/demo-site`, because every external SITE repo\n * consuming `@reblu/site-contracts` carries the identical snippet — publishing the\n * safe component is the only way the fix reaches them.\n */\n\n/**\n * GA4 measurement id: `G-` followed by alphanumeric characters.\n *\n * Vendor source: Google Analytics Help 12270356 (\"formatted as 'G-' followed by\n * alphanumeric characters\", example `G-PSW1MY7HB4`).\n *\n * NO MINIMUM LENGTH, deliberately. The familiar id is ten characters, but Google\n * documents no length at all and its own docs print `gtag('config', 'G-12345')`\n * (Help 11259997) — inventing a floor here would reject an id Google itself\n * publishes. The upper bound is sanity, not security: the `[A-Z0-9]` class already\n * excludes every character that could leave a JS string literal, so length adds\n * nothing to the guarantee.\n *\n * Uppercase only — every documented example is uppercase, and a lowercase variant\n * is not a Google-issued id. Google tag ids also exist as `GT-` and `AW-`\n * (Help 12326985), but this field is specifically the GA4 *measurement id*\n * (the nexus form labels it so, and the SITE reads it as `gaId`); accepting the\n * other prefixes is a product decision, not a security one, and is deliberately\n * left out of this fix.\n */\nexport const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/\n\n/**\n * Microsoft Clarity project id: lowercase alphanumeric.\n *\n * Vendor source: Microsoft Learn `clarity-setup` publishes a live demo project\n * under id `3t0wlogvdz`; every id Microsoft prints is lowercase alphanumeric of\n * about ten characters. Microsoft documents no formal grammar, so — as with GA —\n * the bound is generous and the character class carries the guarantee.\n *\n * RESIDUAL RISK, stated plainly: no vendor page enumerates the legal Clarity\n * alphabet, so this is inferred from published examples. If some tenant holds an\n * id containing a character outside `[a-z0-9]`, this rejects it. That is why the\n * API coerces a non-matching id to `null` instead of erroring — analytics stops\n * loading for that tenant, the site keeps serving, and the misconfiguration is\n * visible and fixable rather than an outage.\n */\nexport const CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/\n\n/** True only for a string matching the vendor-documented GA4 measurement id shape. */\nexport function isValidGaMeasurementId(value: unknown): value is string {\n return typeof value === 'string' && GA_MEASUREMENT_ID_PATTERN.test(value)\n}\n\n/** True only for a string matching the vendor-documented Clarity project id shape. */\nexport function isValidClarityProjectId(value: unknown): value is string {\n return typeof value === 'string' && CLARITY_PROJECT_ID_PATTERN.test(value)\n}\n\n/**\n * Serialises `id` as a JS string literal safe to embed in a `<script>` body.\n *\n * Reuses `jsonLdScriptContent` rather than growing a second escaper: a JSON string\n * literal IS a valid JS string literal, and that helper already does the two things\n * required here — `JSON.stringify` for the quote/backslash/newline boundary, plus\n * `<` `>` `&` U+2028 U+2029 escaping for the HTML raw-text boundary that\n * `JSON.stringify` leaves wide open. One audited primitive, one place to fix.\n */\nexport function analyticsScriptLiteral(id: string): string {\n return jsonLdScriptContent(id)\n}\n\n/** Body of the GA4 bootstrap script. Always escapes; never trusts its caller. */\nexport function gaInitScript(gaId: string): string {\n return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`\n}\n\n/** Body of the Microsoft Clarity bootstrap script. Always escapes; never trusts its caller. */\nexport function clarityInitScript(clarityId: string): string {\n return `(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\",${analyticsScriptLiteral(clarityId)});`\n}\n\nexport interface AnalyticsScriptsProps {\n /** GA4 measurement id from the SITE analytics contract. Ignored unless it matches `G-…`. */\n gaId?: string | null\n /** Microsoft Clarity project id from the SITE analytics contract. Ignored unless lowercase alphanumeric. */\n clarityId?: string | null\n}\n\n/**\n * Renders the Google Analytics and Microsoft Clarity tags for a tenant, dropping\n * any id that does not match its vendor-documented shape.\n */\nexport function AnalyticsScripts({ gaId, clarityId }: AnalyticsScriptsProps) {\n const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null\n const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null\n\n if (!safeGaId && !safeClarityId) return null\n\n return (\n <>\n {safeGaId && (\n <>\n <Script\n src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`}\n strategy='afterInteractive'\n />\n <Script\n id='ga-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: gaInitScript(safeGaId) }}\n />\n </>\n )}\n\n {safeClarityId && (\n <Script\n id='clarity-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: clarityInitScript(safeClarityId) }}\n />\n )}\n </>\n )\n}\n","/**\n * `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).\n *\n * A `<script>` element is HTML's \"raw text\" context: the parser closes it on the\n * first case-insensitive `</script` byte sequence it sees, no matter whether\n * that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON\n * syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any\n * JSON-LD payload built from tenant-authored data (page title, description,\n * FAQ copy, …) into a stored-XSS sink: a value containing\n * `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.\n *\n * `jsonLdScriptContent` neutralises exactly that boundary while staying\n * byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON\n * character escapes, so `JSON.parse` recovers the original value unchanged —\n * this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in\n * `@reblu/site-blocks` for that job, at the HTML boundary rather than the\n * script boundary). U+2028/U+2029 are escaped too: both are valid inside a\n * JSON string but are raw JS line terminators, and some `<script>` consumers\n * (older JS parsers, some static analysers) choke on them unescaped.\n *\n * This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s\n * main entry bundles `parse5` + the `hast-util-*` chain + `marked` for\n * `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848\n * spent a full round removing exactly that weight (89 KB gzipped) from a client\n * bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific\n * (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has\n * zero dependencies of its own, so it ships as its OWN tsup entry\n * (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel\n * — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +\n * `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`\n * already use for the same reason.\n *\n * @see https://nextjs.org/docs/app/guides/json-ld\n */\n\nconst SCRIPT_BOUNDARY_UNSAFE = /[<>&\\u2028\\u2029]/g\n\nconst ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\n/**\n * Serialises `value` for embedding inside `<script type=\"application/ld+json\">`.\n * Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON\n * character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers\n * `value` unchanged. This only ever ADDS escaping; it never strips or rewrites\n * data, unlike an HTML sanitiser.\n *\n * `JSON.stringify(undefined)` returns the JS value `undefined`, not a string\n * (`undefined` has no JSON representation), which would otherwise throw here.\n * `data` is typed `unknown` with zero friction toward `undefined`, so this\n * falls back to the JSON literal `'null'` rather than crashing an entire page\n * render over one missing field.\n */\nexport function jsonLdScriptContent(value: unknown): string {\n const json = JSON.stringify(value) ?? 'null'\n return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char])\n}\n\nexport interface JsonLdProps {\n /** Suffix for the rendered `<script>` id: `json-ld-${id}`. */\n id: string\n /** Structured data to serialise. Any JSON-serialisable value. */\n data: unknown\n}\n\n/**\n * Renders one `application/ld+json` `<script>` tag using the official Next.js\n * App Router pattern. `suppressHydrationWarning` is required here: React warns\n * on a server/client `textContent` mismatch for this element even though there\n * is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's\n * hydration diff.\n */\nexport function JsonLd({ id, data }: JsonLdProps) {\n return (\n <script\n id={`json-ld-${id}`}\n type='application/ld+json'\n suppressHydrationWarning\n dangerouslySetInnerHTML={{ __html: jsonLdScriptContent(data) }}\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAmB;;;AC+Ef;AA5CJ,IAAM,yBAAyB;AAE/B,IAAM,UAAkC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAeO,SAAS,oBAAoB,OAAwB;AAC1D,QAAM,OAAO,KAAK,UAAU,KAAK,KAAK;AACtC,SAAO,KAAK,QAAQ,wBAAwB,CAAC,SAAS,QAAQ,IAAI,CAAC;AACrE;;;AD0EQ,IAAAA,sBAAA;AAxED,IAAM,4BAA4B;AAiBlC,IAAM,6BAA6B;AAGnC,SAAS,uBAAuB,OAAiC;AACtE,SAAO,OAAO,UAAU,YAAY,0BAA0B,KAAK,KAAK;AAC1E;AAGO,SAAS,wBAAwB,OAAiC;AACvE,SAAO,OAAO,UAAU,YAAY,2BAA2B,KAAK,KAAK;AAC3E;AAWO,SAAS,uBAAuB,IAAoB;AACzD,SAAO,oBAAoB,EAAE;AAC/B;AAGO,SAAS,aAAa,MAAsB;AACjD,SAAO,wHAAwH,uBAAuB,IAAI,CAAC;AAC7J;AAGO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,+PAA+P,uBAAuB,SAAS,CAAC;AACzS;AAaO,SAAS,iBAAiB,EAAE,MAAM,UAAU,GAA0B;AAC3E,QAAM,WAAW,uBAAuB,IAAI,IAAI,OAAO;AACvD,QAAM,gBAAgB,wBAAwB,SAAS,IAAI,YAAY;AAEvE,MAAI,CAAC,YAAY,CAAC,cAAe,QAAO;AAExC,SACE,8EACG;AAAA,gBACC,8EACE;AAAA;AAAA,QAAC,cAAAC;AAAA,QAAA;AAAA,UACC,KAAK,+CAA+C,mBAAmB,QAAQ,CAAC;AAAA,UAChF,UAAS;AAAA;AAAA,MACX;AAAA,MACA;AAAA,QAAC,cAAAA;AAAA,QAAA;AAAA,UACC,IAAG;AAAA,UACH,UAAS;AAAA,UACT,yBAAyB,EAAE,QAAQ,aAAa,QAAQ,EAAE;AAAA;AAAA,MAC5D;AAAA,OACF;AAAA,IAGD,iBACC;AAAA,MAAC,cAAAA;AAAA,MAAA;AAAA,QACC,IAAG;AAAA,QACH,UAAS;AAAA,QACT,yBAAyB,EAAE,QAAQ,kBAAkB,aAAa,EAAE;AAAA;AAAA,IACtE;AAAA,KAEJ;AAEJ;","names":["import_jsx_runtime","Script"]}
@@ -0,0 +1,108 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * `<AnalyticsScripts>` — safe rendering of tenant-configured analytics tags (REB-617).
5
+ *
6
+ * THE HOLE THIS CLOSES. Every SITE built on this SDK interpolated the tenant's
7
+ * analytics ids straight into the body of an executing `<script>`:
8
+ *
9
+ * gtag('config','${analytics.gaId}');
10
+ * …"clarity","script","${analytics.clarityId}");
11
+ *
12
+ * No validation, no escaping. A `gaId` of `x');alert(document.cookie);//` closes
13
+ * the string literal, closes the call, comments out the rest, and runs arbitrary
14
+ * JS on the TENANT'S OWN ORIGIN — on every page including checkout, same origin
15
+ * as the portal session cookie. For a medical tenant that is patient-session
16
+ * territory. It is the same trust boundary as the nexus-authored HTML that
17
+ * REB-613 closed on the sink next door, except this one reached the JS
18
+ * interpreter with no sanitiser anywhere in the path.
19
+ *
20
+ * TWO INDEPENDENT LAYERS, because either alone is one refactor away from failing:
21
+ *
22
+ * 1. VALIDATION — an id that does not match its vendor-documented shape is
23
+ * dropped and NO script renders. Analytics silently does not load; the site
24
+ * keeps working. Failing closed matters more than failing loud here: the
25
+ * value is cosmetic, the sink is not.
26
+ * 2. ESCAPING — `analyticsScriptLiteral` emits the id as a JSON string literal
27
+ * with the `<script>` boundary neutralised, so even a value that skipped
28
+ * layer 1 (a future field with a looser shape, a caller that reaches for the
29
+ * builders directly) is inert DATA rather than code.
30
+ *
31
+ * WHY VALIDATION ALONE IS NOT ENOUGH, and escaping alone is not either: layer 1
32
+ * is a policy that a product decision can widen (someone adds `GT-` ids, or a
33
+ * Meta Pixel field whose shape is numeric-but-unbounded), and layer 2 is a
34
+ * mechanism that cannot express "this is not a usable analytics id". Keeping both
35
+ * means widening the policy never reopens the sink.
36
+ *
37
+ * This lives in the SDK, not in `apps/demo-site`, because every external SITE repo
38
+ * consuming `@reblu/site-contracts` carries the identical snippet — publishing the
39
+ * safe component is the only way the fix reaches them.
40
+ */
41
+ /**
42
+ * GA4 measurement id: `G-` followed by alphanumeric characters.
43
+ *
44
+ * Vendor source: Google Analytics Help 12270356 ("formatted as 'G-' followed by
45
+ * alphanumeric characters", example `G-PSW1MY7HB4`).
46
+ *
47
+ * NO MINIMUM LENGTH, deliberately. The familiar id is ten characters, but Google
48
+ * documents no length at all and its own docs print `gtag('config', 'G-12345')`
49
+ * (Help 11259997) — inventing a floor here would reject an id Google itself
50
+ * publishes. The upper bound is sanity, not security: the `[A-Z0-9]` class already
51
+ * excludes every character that could leave a JS string literal, so length adds
52
+ * nothing to the guarantee.
53
+ *
54
+ * Uppercase only — every documented example is uppercase, and a lowercase variant
55
+ * is not a Google-issued id. Google tag ids also exist as `GT-` and `AW-`
56
+ * (Help 12326985), but this field is specifically the GA4 *measurement id*
57
+ * (the nexus form labels it so, and the SITE reads it as `gaId`); accepting the
58
+ * other prefixes is a product decision, not a security one, and is deliberately
59
+ * left out of this fix.
60
+ */
61
+ declare const GA_MEASUREMENT_ID_PATTERN: RegExp;
62
+ /**
63
+ * Microsoft Clarity project id: lowercase alphanumeric.
64
+ *
65
+ * Vendor source: Microsoft Learn `clarity-setup` publishes a live demo project
66
+ * under id `3t0wlogvdz`; every id Microsoft prints is lowercase alphanumeric of
67
+ * about ten characters. Microsoft documents no formal grammar, so — as with GA —
68
+ * the bound is generous and the character class carries the guarantee.
69
+ *
70
+ * RESIDUAL RISK, stated plainly: no vendor page enumerates the legal Clarity
71
+ * alphabet, so this is inferred from published examples. If some tenant holds an
72
+ * id containing a character outside `[a-z0-9]`, this rejects it. That is why the
73
+ * API coerces a non-matching id to `null` instead of erroring — analytics stops
74
+ * loading for that tenant, the site keeps serving, and the misconfiguration is
75
+ * visible and fixable rather than an outage.
76
+ */
77
+ declare const CLARITY_PROJECT_ID_PATTERN: RegExp;
78
+ /** True only for a string matching the vendor-documented GA4 measurement id shape. */
79
+ declare function isValidGaMeasurementId(value: unknown): value is string;
80
+ /** True only for a string matching the vendor-documented Clarity project id shape. */
81
+ declare function isValidClarityProjectId(value: unknown): value is string;
82
+ /**
83
+ * Serialises `id` as a JS string literal safe to embed in a `<script>` body.
84
+ *
85
+ * Reuses `jsonLdScriptContent` rather than growing a second escaper: a JSON string
86
+ * literal IS a valid JS string literal, and that helper already does the two things
87
+ * required here — `JSON.stringify` for the quote/backslash/newline boundary, plus
88
+ * `<` `>` `&` U+2028 U+2029 escaping for the HTML raw-text boundary that
89
+ * `JSON.stringify` leaves wide open. One audited primitive, one place to fix.
90
+ */
91
+ declare function analyticsScriptLiteral(id: string): string;
92
+ /** Body of the GA4 bootstrap script. Always escapes; never trusts its caller. */
93
+ declare function gaInitScript(gaId: string): string;
94
+ /** Body of the Microsoft Clarity bootstrap script. Always escapes; never trusts its caller. */
95
+ declare function clarityInitScript(clarityId: string): string;
96
+ interface AnalyticsScriptsProps {
97
+ /** GA4 measurement id from the SITE analytics contract. Ignored unless it matches `G-…`. */
98
+ gaId?: string | null;
99
+ /** Microsoft Clarity project id from the SITE analytics contract. Ignored unless lowercase alphanumeric. */
100
+ clarityId?: string | null;
101
+ }
102
+ /**
103
+ * Renders the Google Analytics and Microsoft Clarity tags for a tenant, dropping
104
+ * any id that does not match its vendor-documented shape.
105
+ */
106
+ declare function AnalyticsScripts({ gaId, clarityId }: AnalyticsScriptsProps): react_jsx_runtime.JSX.Element | null;
107
+
108
+ export { AnalyticsScripts, type AnalyticsScriptsProps, CLARITY_PROJECT_ID_PATTERN, GA_MEASUREMENT_ID_PATTERN, analyticsScriptLiteral, clarityInitScript, gaInitScript, isValidClarityProjectId, isValidGaMeasurementId };
@@ -0,0 +1,108 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * `<AnalyticsScripts>` — safe rendering of tenant-configured analytics tags (REB-617).
5
+ *
6
+ * THE HOLE THIS CLOSES. Every SITE built on this SDK interpolated the tenant's
7
+ * analytics ids straight into the body of an executing `<script>`:
8
+ *
9
+ * gtag('config','${analytics.gaId}');
10
+ * …"clarity","script","${analytics.clarityId}");
11
+ *
12
+ * No validation, no escaping. A `gaId` of `x');alert(document.cookie);//` closes
13
+ * the string literal, closes the call, comments out the rest, and runs arbitrary
14
+ * JS on the TENANT'S OWN ORIGIN — on every page including checkout, same origin
15
+ * as the portal session cookie. For a medical tenant that is patient-session
16
+ * territory. It is the same trust boundary as the nexus-authored HTML that
17
+ * REB-613 closed on the sink next door, except this one reached the JS
18
+ * interpreter with no sanitiser anywhere in the path.
19
+ *
20
+ * TWO INDEPENDENT LAYERS, because either alone is one refactor away from failing:
21
+ *
22
+ * 1. VALIDATION — an id that does not match its vendor-documented shape is
23
+ * dropped and NO script renders. Analytics silently does not load; the site
24
+ * keeps working. Failing closed matters more than failing loud here: the
25
+ * value is cosmetic, the sink is not.
26
+ * 2. ESCAPING — `analyticsScriptLiteral` emits the id as a JSON string literal
27
+ * with the `<script>` boundary neutralised, so even a value that skipped
28
+ * layer 1 (a future field with a looser shape, a caller that reaches for the
29
+ * builders directly) is inert DATA rather than code.
30
+ *
31
+ * WHY VALIDATION ALONE IS NOT ENOUGH, and escaping alone is not either: layer 1
32
+ * is a policy that a product decision can widen (someone adds `GT-` ids, or a
33
+ * Meta Pixel field whose shape is numeric-but-unbounded), and layer 2 is a
34
+ * mechanism that cannot express "this is not a usable analytics id". Keeping both
35
+ * means widening the policy never reopens the sink.
36
+ *
37
+ * This lives in the SDK, not in `apps/demo-site`, because every external SITE repo
38
+ * consuming `@reblu/site-contracts` carries the identical snippet — publishing the
39
+ * safe component is the only way the fix reaches them.
40
+ */
41
+ /**
42
+ * GA4 measurement id: `G-` followed by alphanumeric characters.
43
+ *
44
+ * Vendor source: Google Analytics Help 12270356 ("formatted as 'G-' followed by
45
+ * alphanumeric characters", example `G-PSW1MY7HB4`).
46
+ *
47
+ * NO MINIMUM LENGTH, deliberately. The familiar id is ten characters, but Google
48
+ * documents no length at all and its own docs print `gtag('config', 'G-12345')`
49
+ * (Help 11259997) — inventing a floor here would reject an id Google itself
50
+ * publishes. The upper bound is sanity, not security: the `[A-Z0-9]` class already
51
+ * excludes every character that could leave a JS string literal, so length adds
52
+ * nothing to the guarantee.
53
+ *
54
+ * Uppercase only — every documented example is uppercase, and a lowercase variant
55
+ * is not a Google-issued id. Google tag ids also exist as `GT-` and `AW-`
56
+ * (Help 12326985), but this field is specifically the GA4 *measurement id*
57
+ * (the nexus form labels it so, and the SITE reads it as `gaId`); accepting the
58
+ * other prefixes is a product decision, not a security one, and is deliberately
59
+ * left out of this fix.
60
+ */
61
+ declare const GA_MEASUREMENT_ID_PATTERN: RegExp;
62
+ /**
63
+ * Microsoft Clarity project id: lowercase alphanumeric.
64
+ *
65
+ * Vendor source: Microsoft Learn `clarity-setup` publishes a live demo project
66
+ * under id `3t0wlogvdz`; every id Microsoft prints is lowercase alphanumeric of
67
+ * about ten characters. Microsoft documents no formal grammar, so — as with GA —
68
+ * the bound is generous and the character class carries the guarantee.
69
+ *
70
+ * RESIDUAL RISK, stated plainly: no vendor page enumerates the legal Clarity
71
+ * alphabet, so this is inferred from published examples. If some tenant holds an
72
+ * id containing a character outside `[a-z0-9]`, this rejects it. That is why the
73
+ * API coerces a non-matching id to `null` instead of erroring — analytics stops
74
+ * loading for that tenant, the site keeps serving, and the misconfiguration is
75
+ * visible and fixable rather than an outage.
76
+ */
77
+ declare const CLARITY_PROJECT_ID_PATTERN: RegExp;
78
+ /** True only for a string matching the vendor-documented GA4 measurement id shape. */
79
+ declare function isValidGaMeasurementId(value: unknown): value is string;
80
+ /** True only for a string matching the vendor-documented Clarity project id shape. */
81
+ declare function isValidClarityProjectId(value: unknown): value is string;
82
+ /**
83
+ * Serialises `id` as a JS string literal safe to embed in a `<script>` body.
84
+ *
85
+ * Reuses `jsonLdScriptContent` rather than growing a second escaper: a JSON string
86
+ * literal IS a valid JS string literal, and that helper already does the two things
87
+ * required here — `JSON.stringify` for the quote/backslash/newline boundary, plus
88
+ * `<` `>` `&` U+2028 U+2029 escaping for the HTML raw-text boundary that
89
+ * `JSON.stringify` leaves wide open. One audited primitive, one place to fix.
90
+ */
91
+ declare function analyticsScriptLiteral(id: string): string;
92
+ /** Body of the GA4 bootstrap script. Always escapes; never trusts its caller. */
93
+ declare function gaInitScript(gaId: string): string;
94
+ /** Body of the Microsoft Clarity bootstrap script. Always escapes; never trusts its caller. */
95
+ declare function clarityInitScript(clarityId: string): string;
96
+ interface AnalyticsScriptsProps {
97
+ /** GA4 measurement id from the SITE analytics contract. Ignored unless it matches `G-…`. */
98
+ gaId?: string | null;
99
+ /** Microsoft Clarity project id from the SITE analytics contract. Ignored unless lowercase alphanumeric. */
100
+ clarityId?: string | null;
101
+ }
102
+ /**
103
+ * Renders the Google Analytics and Microsoft Clarity tags for a tenant, dropping
104
+ * any id that does not match its vendor-documented shape.
105
+ */
106
+ declare function AnalyticsScripts({ gaId, clarityId }: AnalyticsScriptsProps): react_jsx_runtime.JSX.Element | null;
107
+
108
+ export { AnalyticsScripts, type AnalyticsScriptsProps, CLARITY_PROJECT_ID_PATTERN, GA_MEASUREMENT_ID_PATTERN, analyticsScriptLiteral, clarityInitScript, gaInitScript, isValidClarityProjectId, isValidGaMeasurementId };
@@ -0,0 +1,80 @@
1
+ // src/analytics.tsx
2
+ import Script from "next/script";
3
+
4
+ // src/json-ld.tsx
5
+ import { jsx } from "react/jsx-runtime";
6
+ var SCRIPT_BOUNDARY_UNSAFE = /[<>&\u2028\u2029]/g;
7
+ var ESCAPES = {
8
+ "<": "\\u003c",
9
+ ">": "\\u003e",
10
+ "&": "\\u0026",
11
+ "\u2028": "\\u2028",
12
+ "\u2029": "\\u2029"
13
+ };
14
+ function jsonLdScriptContent(value) {
15
+ const json = JSON.stringify(value) ?? "null";
16
+ return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char]);
17
+ }
18
+
19
+ // src/analytics.tsx
20
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
21
+ var GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/;
22
+ var CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/;
23
+ function isValidGaMeasurementId(value) {
24
+ return typeof value === "string" && GA_MEASUREMENT_ID_PATTERN.test(value);
25
+ }
26
+ function isValidClarityProjectId(value) {
27
+ return typeof value === "string" && CLARITY_PROJECT_ID_PATTERN.test(value);
28
+ }
29
+ function analyticsScriptLiteral(id) {
30
+ return jsonLdScriptContent(id);
31
+ }
32
+ function gaInitScript(gaId) {
33
+ return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`;
34
+ }
35
+ function clarityInitScript(clarityId) {
36
+ return `(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",${analyticsScriptLiteral(clarityId)});`;
37
+ }
38
+ function AnalyticsScripts({ gaId, clarityId }) {
39
+ const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null;
40
+ const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null;
41
+ if (!safeGaId && !safeClarityId) return null;
42
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
43
+ safeGaId && /* @__PURE__ */ jsxs(Fragment, { children: [
44
+ /* @__PURE__ */ jsx2(
45
+ Script,
46
+ {
47
+ src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`,
48
+ strategy: "afterInteractive"
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx2(
52
+ Script,
53
+ {
54
+ id: "ga-init",
55
+ strategy: "afterInteractive",
56
+ dangerouslySetInnerHTML: { __html: gaInitScript(safeGaId) }
57
+ }
58
+ )
59
+ ] }),
60
+ safeClarityId && /* @__PURE__ */ jsx2(
61
+ Script,
62
+ {
63
+ id: "clarity-init",
64
+ strategy: "afterInteractive",
65
+ dangerouslySetInnerHTML: { __html: clarityInitScript(safeClarityId) }
66
+ }
67
+ )
68
+ ] });
69
+ }
70
+ export {
71
+ AnalyticsScripts,
72
+ CLARITY_PROJECT_ID_PATTERN,
73
+ GA_MEASUREMENT_ID_PATTERN,
74
+ analyticsScriptLiteral,
75
+ clarityInitScript,
76
+ gaInitScript,
77
+ isValidClarityProjectId,
78
+ isValidGaMeasurementId
79
+ };
80
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analytics.tsx","../src/json-ld.tsx"],"sourcesContent":["import Script from 'next/script'\n\nimport { jsonLdScriptContent } from './json-ld'\n\n/**\n * `<AnalyticsScripts>` — safe rendering of tenant-configured analytics tags (REB-617).\n *\n * THE HOLE THIS CLOSES. Every SITE built on this SDK interpolated the tenant's\n * analytics ids straight into the body of an executing `<script>`:\n *\n * gtag('config','${analytics.gaId}');\n * …\"clarity\",\"script\",\"${analytics.clarityId}\");\n *\n * No validation, no escaping. A `gaId` of `x');alert(document.cookie);//` closes\n * the string literal, closes the call, comments out the rest, and runs arbitrary\n * JS on the TENANT'S OWN ORIGIN — on every page including checkout, same origin\n * as the portal session cookie. For a medical tenant that is patient-session\n * territory. It is the same trust boundary as the nexus-authored HTML that\n * REB-613 closed on the sink next door, except this one reached the JS\n * interpreter with no sanitiser anywhere in the path.\n *\n * TWO INDEPENDENT LAYERS, because either alone is one refactor away from failing:\n *\n * 1. VALIDATION — an id that does not match its vendor-documented shape is\n * dropped and NO script renders. Analytics silently does not load; the site\n * keeps working. Failing closed matters more than failing loud here: the\n * value is cosmetic, the sink is not.\n * 2. ESCAPING — `analyticsScriptLiteral` emits the id as a JSON string literal\n * with the `<script>` boundary neutralised, so even a value that skipped\n * layer 1 (a future field with a looser shape, a caller that reaches for the\n * builders directly) is inert DATA rather than code.\n *\n * WHY VALIDATION ALONE IS NOT ENOUGH, and escaping alone is not either: layer 1\n * is a policy that a product decision can widen (someone adds `GT-` ids, or a\n * Meta Pixel field whose shape is numeric-but-unbounded), and layer 2 is a\n * mechanism that cannot express \"this is not a usable analytics id\". Keeping both\n * means widening the policy never reopens the sink.\n *\n * This lives in the SDK, not in `apps/demo-site`, because every external SITE repo\n * consuming `@reblu/site-contracts` carries the identical snippet — publishing the\n * safe component is the only way the fix reaches them.\n */\n\n/**\n * GA4 measurement id: `G-` followed by alphanumeric characters.\n *\n * Vendor source: Google Analytics Help 12270356 (\"formatted as 'G-' followed by\n * alphanumeric characters\", example `G-PSW1MY7HB4`).\n *\n * NO MINIMUM LENGTH, deliberately. The familiar id is ten characters, but Google\n * documents no length at all and its own docs print `gtag('config', 'G-12345')`\n * (Help 11259997) — inventing a floor here would reject an id Google itself\n * publishes. The upper bound is sanity, not security: the `[A-Z0-9]` class already\n * excludes every character that could leave a JS string literal, so length adds\n * nothing to the guarantee.\n *\n * Uppercase only — every documented example is uppercase, and a lowercase variant\n * is not a Google-issued id. Google tag ids also exist as `GT-` and `AW-`\n * (Help 12326985), but this field is specifically the GA4 *measurement id*\n * (the nexus form labels it so, and the SITE reads it as `gaId`); accepting the\n * other prefixes is a product decision, not a security one, and is deliberately\n * left out of this fix.\n */\nexport const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/\n\n/**\n * Microsoft Clarity project id: lowercase alphanumeric.\n *\n * Vendor source: Microsoft Learn `clarity-setup` publishes a live demo project\n * under id `3t0wlogvdz`; every id Microsoft prints is lowercase alphanumeric of\n * about ten characters. Microsoft documents no formal grammar, so — as with GA —\n * the bound is generous and the character class carries the guarantee.\n *\n * RESIDUAL RISK, stated plainly: no vendor page enumerates the legal Clarity\n * alphabet, so this is inferred from published examples. If some tenant holds an\n * id containing a character outside `[a-z0-9]`, this rejects it. That is why the\n * API coerces a non-matching id to `null` instead of erroring — analytics stops\n * loading for that tenant, the site keeps serving, and the misconfiguration is\n * visible and fixable rather than an outage.\n */\nexport const CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/\n\n/** True only for a string matching the vendor-documented GA4 measurement id shape. */\nexport function isValidGaMeasurementId(value: unknown): value is string {\n return typeof value === 'string' && GA_MEASUREMENT_ID_PATTERN.test(value)\n}\n\n/** True only for a string matching the vendor-documented Clarity project id shape. */\nexport function isValidClarityProjectId(value: unknown): value is string {\n return typeof value === 'string' && CLARITY_PROJECT_ID_PATTERN.test(value)\n}\n\n/**\n * Serialises `id` as a JS string literal safe to embed in a `<script>` body.\n *\n * Reuses `jsonLdScriptContent` rather than growing a second escaper: a JSON string\n * literal IS a valid JS string literal, and that helper already does the two things\n * required here — `JSON.stringify` for the quote/backslash/newline boundary, plus\n * `<` `>` `&` U+2028 U+2029 escaping for the HTML raw-text boundary that\n * `JSON.stringify` leaves wide open. One audited primitive, one place to fix.\n */\nexport function analyticsScriptLiteral(id: string): string {\n return jsonLdScriptContent(id)\n}\n\n/** Body of the GA4 bootstrap script. Always escapes; never trusts its caller. */\nexport function gaInitScript(gaId: string): string {\n return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`\n}\n\n/** Body of the Microsoft Clarity bootstrap script. Always escapes; never trusts its caller. */\nexport function clarityInitScript(clarityId: string): string {\n return `(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\",${analyticsScriptLiteral(clarityId)});`\n}\n\nexport interface AnalyticsScriptsProps {\n /** GA4 measurement id from the SITE analytics contract. Ignored unless it matches `G-…`. */\n gaId?: string | null\n /** Microsoft Clarity project id from the SITE analytics contract. Ignored unless lowercase alphanumeric. */\n clarityId?: string | null\n}\n\n/**\n * Renders the Google Analytics and Microsoft Clarity tags for a tenant, dropping\n * any id that does not match its vendor-documented shape.\n */\nexport function AnalyticsScripts({ gaId, clarityId }: AnalyticsScriptsProps) {\n const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null\n const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null\n\n if (!safeGaId && !safeClarityId) return null\n\n return (\n <>\n {safeGaId && (\n <>\n <Script\n src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`}\n strategy='afterInteractive'\n />\n <Script\n id='ga-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: gaInitScript(safeGaId) }}\n />\n </>\n )}\n\n {safeClarityId && (\n <Script\n id='clarity-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: clarityInitScript(safeClarityId) }}\n />\n )}\n </>\n )\n}\n","/**\n * `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).\n *\n * A `<script>` element is HTML's \"raw text\" context: the parser closes it on the\n * first case-insensitive `</script` byte sequence it sees, no matter whether\n * that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON\n * syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any\n * JSON-LD payload built from tenant-authored data (page title, description,\n * FAQ copy, …) into a stored-XSS sink: a value containing\n * `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.\n *\n * `jsonLdScriptContent` neutralises exactly that boundary while staying\n * byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON\n * character escapes, so `JSON.parse` recovers the original value unchanged —\n * this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in\n * `@reblu/site-blocks` for that job, at the HTML boundary rather than the\n * script boundary). U+2028/U+2029 are escaped too: both are valid inside a\n * JSON string but are raw JS line terminators, and some `<script>` consumers\n * (older JS parsers, some static analysers) choke on them unescaped.\n *\n * This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s\n * main entry bundles `parse5` + the `hast-util-*` chain + `marked` for\n * `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848\n * spent a full round removing exactly that weight (89 KB gzipped) from a client\n * bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific\n * (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has\n * zero dependencies of its own, so it ships as its OWN tsup entry\n * (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel\n * — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +\n * `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`\n * already use for the same reason.\n *\n * @see https://nextjs.org/docs/app/guides/json-ld\n */\n\nconst SCRIPT_BOUNDARY_UNSAFE = /[<>&\\u2028\\u2029]/g\n\nconst ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\n/**\n * Serialises `value` for embedding inside `<script type=\"application/ld+json\">`.\n * Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON\n * character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers\n * `value` unchanged. This only ever ADDS escaping; it never strips or rewrites\n * data, unlike an HTML sanitiser.\n *\n * `JSON.stringify(undefined)` returns the JS value `undefined`, not a string\n * (`undefined` has no JSON representation), which would otherwise throw here.\n * `data` is typed `unknown` with zero friction toward `undefined`, so this\n * falls back to the JSON literal `'null'` rather than crashing an entire page\n * render over one missing field.\n */\nexport function jsonLdScriptContent(value: unknown): string {\n const json = JSON.stringify(value) ?? 'null'\n return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char])\n}\n\nexport interface JsonLdProps {\n /** Suffix for the rendered `<script>` id: `json-ld-${id}`. */\n id: string\n /** Structured data to serialise. Any JSON-serialisable value. */\n data: unknown\n}\n\n/**\n * Renders one `application/ld+json` `<script>` tag using the official Next.js\n * App Router pattern. `suppressHydrationWarning` is required here: React warns\n * on a server/client `textContent` mismatch for this element even though there\n * is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's\n * hydration diff.\n */\nexport function JsonLd({ id, data }: JsonLdProps) {\n return (\n <script\n id={`json-ld-${id}`}\n type='application/ld+json'\n suppressHydrationWarning\n dangerouslySetInnerHTML={{ __html: jsonLdScriptContent(data) }}\n />\n )\n}\n"],"mappings":";AAAA,OAAO,YAAY;;;AC+Ef;AA5CJ,IAAM,yBAAyB;AAE/B,IAAM,UAAkC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAeO,SAAS,oBAAoB,OAAwB;AAC1D,QAAM,OAAO,KAAK,UAAU,KAAK,KAAK;AACtC,SAAO,KAAK,QAAQ,wBAAwB,CAAC,SAAS,QAAQ,IAAI,CAAC;AACrE;;;AD0EQ,mBACE,OAAAA,MADF;AAxED,IAAM,4BAA4B;AAiBlC,IAAM,6BAA6B;AAGnC,SAAS,uBAAuB,OAAiC;AACtE,SAAO,OAAO,UAAU,YAAY,0BAA0B,KAAK,KAAK;AAC1E;AAGO,SAAS,wBAAwB,OAAiC;AACvE,SAAO,OAAO,UAAU,YAAY,2BAA2B,KAAK,KAAK;AAC3E;AAWO,SAAS,uBAAuB,IAAoB;AACzD,SAAO,oBAAoB,EAAE;AAC/B;AAGO,SAAS,aAAa,MAAsB;AACjD,SAAO,wHAAwH,uBAAuB,IAAI,CAAC;AAC7J;AAGO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,+PAA+P,uBAAuB,SAAS,CAAC;AACzS;AAaO,SAAS,iBAAiB,EAAE,MAAM,UAAU,GAA0B;AAC3E,QAAM,WAAW,uBAAuB,IAAI,IAAI,OAAO;AACvD,QAAM,gBAAgB,wBAAwB,SAAS,IAAI,YAAY;AAEvE,MAAI,CAAC,YAAY,CAAC,cAAe,QAAO;AAExC,SACE,iCACG;AAAA,gBACC,iCACE;AAAA,sBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,+CAA+C,mBAAmB,QAAQ,CAAC;AAAA,UAChF,UAAS;AAAA;AAAA,MACX;AAAA,MACA,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,UAAS;AAAA,UACT,yBAAyB,EAAE,QAAQ,aAAa,QAAQ,EAAE;AAAA;AAAA,MAC5D;AAAA,OACF;AAAA,IAGD,iBACC,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,IAAG;AAAA,QACH,UAAS;AAAA,QACT,yBAAyB,EAAE,QAAQ,kBAAkB,aAAa,EAAE;AAAA;AAAA,IACtE;AAAA,KAEJ;AAEJ;","names":["jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reblu/site-next",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Next.js RSC glue for Reblu SITEs — cache + CTA + auth server/client components.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Reblu — Adolfo Unturbe",
@@ -90,6 +90,16 @@
90
90
  "types": "./dist/json-ld.d.cts",
91
91
  "default": "./dist/json-ld.cjs"
92
92
  }
93
+ },
94
+ "./analytics": {
95
+ "import": {
96
+ "types": "./dist/analytics.d.ts",
97
+ "default": "./dist/analytics.js"
98
+ },
99
+ "require": {
100
+ "types": "./dist/analytics.d.cts",
101
+ "default": "./dist/analytics.cjs"
102
+ }
93
103
  }
94
104
  },
95
105
  "publishConfig": {
@@ -101,9 +111,9 @@
101
111
  "react": ">=19"
102
112
  },
103
113
  "dependencies": {
104
- "@reblu/site-client": "^3.0.0",
105
- "@reblu/site-ui": "^0.3.1",
106
- "@reblu/site-contracts": "^0.4.0"
114
+ "@reblu/site-client": "^4.0.0",
115
+ "@reblu/site-contracts": "^0.5.0",
116
+ "@reblu/site-ui": "^0.4.0"
107
117
  },
108
118
  "devDependencies": {
109
119
  "@testing-library/jest-dom": "^6.9.1",