@tokenoftrust/storefront-runner 2.0.30 → 2.0.32

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 (37) hide show
  1. package/apps/storefront/astro.config.mjs +2 -0
  2. package/apps/storefront/env.d.ts +0 -2
  3. package/apps/storefront/integrations/google-measurement-sandbox-asset.mjs +97 -0
  4. package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +13 -0
  5. package/apps/storefront/src/layouts/Layout.astro +3 -48
  6. package/apps/storefront/src/lib/analytics/budgets.json +0 -1
  7. package/apps/storefront/src/lib/analytics/index.ts +1 -4
  8. package/apps/storefront/src/lib/analytics/webVitals.ts +42 -4
  9. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +1 -1
  10. package/apps/storefront/src/lib/monitoring/manifest.ts +1 -2
  11. package/package.json +1 -1
  12. package/packages/public-runtime/package.json +2 -1
  13. package/packages/public-runtime/src/csp.ts +2 -2
  14. package/packages/public-runtime/src/integration/consent-broker.ts +370 -13
  15. package/packages/public-runtime/src/integration/csp-compiler.ts +3 -6
  16. package/packages/public-runtime/src/integration/events.ts +5 -5
  17. package/packages/public-runtime/src/integration/index.ts +1 -0
  18. package/packages/public-runtime/src/integration/interaction-tracking-gate.ts +1 -2
  19. package/packages/public-runtime/src/integration/lifecycle-boot.ts +28 -14
  20. package/packages/public-runtime/src/integration/lifecycle-host-entry.ts +2 -0
  21. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +256 -22
  22. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +9 -0
  23. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
  24. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +3 -0
  25. package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +207 -0
  26. package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +168 -0
  27. package/packages/public-runtime/src/integration/native-ga4.ts +111 -215
  28. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +18 -9
  29. package/packages/public-runtime/src/integration/platform-adapters.ts +20 -9
  30. package/packages/public-runtime/src/integration/render-phases.ts +28 -2
  31. package/packages/public-runtime/src/integration/version.ts +2 -2
  32. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +1 -1
  33. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +1 -1
  34. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +1 -1
  35. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +1 -1
  36. package/scripts/tenant/lint-embeds.mjs +0 -8
  37. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +0 -19
@@ -17,11 +17,11 @@ import {
17
17
  createAmplitudeLifecycleModule,
18
18
  } from "./native-amplitude.js";
19
19
  import {
20
- createNativeGa4FetchTransport,
21
20
  createNativeGa4Module,
22
21
  NATIVE_GA4_ADAPTER_VERSION,
23
22
  NATIVE_GA4_ENTRY_URL,
24
23
  NATIVE_GA4_RUNTIME_ID,
24
+ type NativeGa4Transport,
25
25
  } from "./native-ga4.js";
26
26
  import { AMPLITUDE_HTTP_ADAPTER, GOOGLE_MEASUREMENT_ADAPTER } from "./platform-adapters.js";
27
27
 
@@ -38,7 +38,8 @@ export interface PlatformAdapterEntryMarker {
38
38
  }
39
39
 
40
40
  export interface PlatformAdapterRuntimeHost {
41
- fetch: typeof fetch;
41
+ fetch?: typeof fetch;
42
+ nativeGa4Transport?: NativeGa4Transport;
42
43
  }
43
44
 
44
45
  export interface PlatformAdapterRuntimeRegistration {
@@ -70,19 +71,23 @@ const AMPLITUDE_MARKER: PlatformAdapterEntryMarker = Object.freeze({
70
71
  export const PLATFORM_ADAPTER_RUNTIME_REGISTRY: readonly PlatformAdapterRuntimeRegistration[] = Object.freeze([
71
72
  Object.freeze({
72
73
  marker: GA4_MARKER,
73
- create: ({ fetch: fetchCapability }: PlatformAdapterRuntimeHost) => createNativeGa4Module({
74
- transport: createNativeGa4FetchTransport(fetchCapability),
75
- }),
74
+ create: ({ nativeGa4Transport }: PlatformAdapterRuntimeHost) => {
75
+ if (!nativeGa4Transport) throw new Error("platform_adapter_ga4_sandbox_unavailable");
76
+ return createNativeGa4Module({ transport: nativeGa4Transport });
77
+ },
76
78
  }),
77
79
  Object.freeze({
78
80
  marker: AMPLITUDE_MARKER,
79
- create: ({ fetch: fetchCapability }: PlatformAdapterRuntimeHost) =>
80
- createAmplitudeLifecycleModule({ fetch: fetchCapability }),
81
+ create: ({ fetch: fetchCapability }: PlatformAdapterRuntimeHost) => {
82
+ if (!fetchCapability) throw new Error("platform_adapter_fetch_unavailable");
83
+ return createAmplitudeLifecycleModule({ fetch: fetchCapability });
84
+ },
81
85
  }),
82
86
  ]);
83
87
 
84
88
  export interface PlatformAdapterRuntimeLoaderOptions {
85
89
  fetchCapability?: typeof fetch;
90
+ nativeGa4Transport?: () => NativeGa4Transport;
86
91
  importModule?: (url: string) => Promise<unknown>;
87
92
  fallback: IntegrationModuleLoader;
88
93
  }
@@ -137,10 +142,14 @@ export function makePlatformAdapterRuntimeLoader(
137
142
  if (!platformClaim(integration)) return options.fallback(integration);
138
143
  const registration = registrationFor(integration);
139
144
  if (!registration) throw new Error("platform_adapter_entry_unreviewed");
140
- if (!options.fetchCapability) throw new Error("platform_adapter_fetch_unavailable");
141
145
  const namespace = await importModule(registration.marker.entry.url);
142
146
  const marker = (namespace as { platformAdapterEntry?: unknown })?.platformAdapterEntry;
143
147
  if (!markerEquals(registration.marker, marker)) throw new Error("platform_adapter_entry_invalid");
144
- return registration.create({ fetch: options.fetchCapability });
148
+ return registration.create({
149
+ fetch: options.fetchCapability,
150
+ nativeGa4Transport: registration.marker.appId === GOOGLE_MEASUREMENT_ADAPTER.adapterId
151
+ ? options.nativeGa4Transport?.()
152
+ : undefined,
153
+ });
145
154
  };
146
155
  }
@@ -107,6 +107,8 @@ export interface PlatformAdapterDefinition {
107
107
  supportUrl: string;
108
108
  /** The ONE platform-owned entry module every instance of this adapter points at. */
109
109
  entryAsset: string;
110
+ /** Vendor origins used inside the reviewed adapter, independent of parent CSP effects. */
111
+ collectionOrigins: readonly string[];
110
112
  /** The curated CSP profile slug this adapter's origins come from. */
111
113
  cspProfile: IntegrationCspProfile;
112
114
  /** The consent purpose the destination depends on (the broker enforces it). */
@@ -137,10 +139,10 @@ const META_PIXEL_ID = /^\d{10,20}$/;
137
139
  const AMPLITUDE_PROJECT_KEY = /^(?=[a-f0-9]{32}$)(?=.*[a-f])[a-f0-9]+$/i;
138
140
 
139
141
  /**
140
- * Native Google Analytics 4 measurement. A single public measurement id drives
141
- * the platform adapter, which sends reviewed event payloads to the curated analytics
142
- * endpoints in the `google-measurement` CSP profile. It does not run a merchant-owned
143
- * browser container. Depends on analytics consent (ADR 0023 Q3).
142
+ * Native Google Analytics 4 measurement. Reviewed parent code sends a closed event
143
+ * protocol to supported gtag.js inside a null-origin iframe. Google code and network
144
+ * authority stay out of the parent document. It does not run a merchant-owned browser
145
+ * container. Depends on analytics consent (ADR 0023 Q3).
144
146
  */
145
147
  export const GOOGLE_MEASUREMENT_ADAPTER: PlatformAdapterDefinition = Object.freeze({
146
148
  adapterId: "platform.google-measurement",
@@ -148,20 +150,24 @@ export const GOOGLE_MEASUREMENT_ADAPTER: PlatformAdapterDefinition = Object.free
148
150
  purpose: "Native Google Analytics 4 measurement.",
149
151
  publisher: "Storefront platform adapter",
150
152
  supportUrl: "https://developers.google.com/analytics/devguides/collection/ga4",
151
- entryAsset: "/platform/google-measurement/v1/entry.mjs",
153
+ entryAsset: "/platform/google-measurement/v2/entry.mjs",
154
+ collectionOrigins: Object.freeze([
155
+ "https://www.googletagmanager.com",
156
+ "https://www.google-analytics.com",
157
+ "https://analytics.google.com",
158
+ ]),
152
159
  cspProfile: "google-measurement",
153
160
  consentPurpose: "analytics",
154
161
  phase: "document.deferred",
155
162
  performanceBudget: Object.freeze({
156
163
  maxTransferredBytes: 90_000,
157
- maxAdditionalRequests: 4,
164
+ maxAdditionalRequests: 5,
158
165
  maxMainThreadMilliseconds: 120,
159
166
  mustBeAsyncOrDeferred: true,
160
167
  }),
161
168
  idLabel: "GA4 measurement id (e.g. G-XXXXXXX)",
162
- // GA4 natively receives one event at several properties (a primary plus a rollup); the
163
- // wire shape is one `/g/collect` request per measurement id, so the ceiling is the
164
- // budget's own `maxAdditionalRequests`.
169
+ // One gtag.js loader plus one event request per configured property (primary + rollups).
170
+ // The four-destination ceiling therefore consumes the five-request budget exactly.
165
171
  maxDestinationsPerStage: 4,
166
172
  validateId: (id: string) => GOOGLE_MEASUREMENT_ID.test(id),
167
173
  eventMap: Object.freeze({
@@ -183,6 +189,7 @@ export const META_PIXEL_ADAPTER: PlatformAdapterDefinition = Object.freeze({
183
189
  publisher: "Storefront platform adapter",
184
190
  supportUrl: "https://developers.facebook.com/docs/meta-pixel",
185
191
  entryAsset: "platform/meta-pixel/v1/entry.mjs",
192
+ collectionOrigins: Object.freeze(["https://connect.facebook.net", "https://www.facebook.com"]),
186
193
  cspProfile: "meta-pixel",
187
194
  consentPurpose: "marketing",
188
195
  phase: "document.deferred",
@@ -217,6 +224,7 @@ export const AMPLITUDE_HTTP_ADAPTER: PlatformAdapterDefinition = Object.freeze({
217
224
  publisher: "Storefront platform adapter",
218
225
  supportUrl: "https://amplitude.com/docs/apis/analytics/http-v2",
219
226
  entryAsset: "/platform/amplitude-http/v1/entry.mjs",
227
+ collectionOrigins: Object.freeze(["https://api2.amplitude.com"]),
220
228
  cspProfile: "amplitude-http",
221
229
  consentPurpose: "analytics",
222
230
  phase: "document.deferred",
@@ -347,6 +355,8 @@ export interface PlatformAdapterCard {
347
355
  performanceBudget: PerformanceBudget;
348
356
  /** The exact CSP origins this adapter contributes (flattened, sorted). */
349
357
  cspOrigins: string[];
358
+ /** Vendor collection/loading origins, even when isolated from the parent CSP. */
359
+ collectionOrigins: string[];
350
360
  /** The semantic events this adapter measures. */
351
361
  eventTypes: CommerceEventType[];
352
362
  /** Which stages have a configured destination id, and whether each is conversion-live. */
@@ -415,6 +425,7 @@ export function buildPlatformAdapterCard(
415
425
  consentPurposes: [def.consentPurpose],
416
426
  performanceBudget: def.performanceBudget,
417
427
  cspOrigins: profileOrigins(def.cspProfile),
428
+ collectionOrigins: [...def.collectionOrigins].sort(),
418
429
  eventTypes: adapterEventTypes(def),
419
430
  destinationBehavior: stageBehavior(ids),
420
431
  };
@@ -40,6 +40,7 @@ import {
40
40
  } from "./environment.js";
41
41
  import { defaultConsentSnapshot, type ConsentSnapshot } from "./consent-broker.js";
42
42
  import { LIFECYCLE_HOST_ASSET_URL } from "./lifecycle-host-asset.mjs";
43
+ import { VIEWER_ANALYTICS_CONSENT_EARLY_BOOT_JS } from "./consent-broker.js";
43
44
 
44
45
  /** Re-exported so consumers and tests read the host URL from the engine that emits it. */
45
46
  export { LIFECYCLE_HOST_ASSET_PATH, LIFECYCLE_HOST_ASSET_URL } from "./lifecycle-host-asset.mjs";
@@ -163,10 +164,14 @@ export function renderIntegrationPhases(
163
164
  phaseBoundary("consent.defaults") +
164
165
  inlineFrozenGlobal(INTEGRATION_CONSENT_GLOBAL, consentSnapshot, nonce) +
165
166
  phaseBoundary("head.early") +
166
- inlineFrozenGlobal(INTEGRATION_PLAN_GLOBAL, bootDescriptor(plan), nonce);
167
+ inlineFrozenGlobal(INTEGRATION_PLAN_GLOBAL, bootDescriptor(plan), nonce) +
168
+ inlineScript(VIEWER_ANALYTICS_CONSENT_EARLY_BOOT_JS, nonce) +
169
+ viewerAnalyticsConsentStyle(nonce);
167
170
 
168
171
  const bodyStart =
169
- phaseBoundary("body.start") + `<div data-tot-integration-root hidden></div>`;
172
+ phaseBoundary("body.start") +
173
+ `<div data-tot-integration-root hidden></div>` +
174
+ viewerAnalyticsConsentControl();
170
175
 
171
176
  const bodyDeferred =
172
177
  phaseBoundary("document.deferred") +
@@ -196,6 +201,27 @@ function lifecycleHostScript(): string {
196
201
  return `<script type="module" src="${LIFECYCLE_HOST_ASSET_URL}"></script>`;
197
202
  }
198
203
 
204
+ function inlineScript(source: string, nonce: string): string {
205
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
206
+ return `<script${nonceAttr}>${source}</script>`;
207
+ }
208
+
209
+ /** Renderer-owned, provider-neutral viewer control: identical in both document mounts. */
210
+ function viewerAnalyticsConsentStyle(nonce: string): string {
211
+ const style = `.tot-analytics-consent{position:fixed;z-index:900;right:1rem;bottom:1rem;display:flex;max-width:min(30rem,calc(100vw - 2rem));align-items:center;gap:.65rem;padding:.7rem .8rem;border:1px solid var(--color-border,#cbd5e1);border-radius:.75rem;background:var(--color-bg,#fff);color:var(--color-text,#111827);box-shadow:0 8px 30px rgba(15,23,42,.18);font:500 .75rem/1.25 system-ui,sans-serif}.tot-analytics-consent__copy{display:grid;gap:.15rem;min-width:8rem}.tot-analytics-consent__detail{color:var(--color-muted,#475569);font-size:.68rem}.tot-analytics-consent__actions{display:flex;gap:.4rem}.tot-analytics-consent button{border:1px solid currentColor;border-radius:999px;background:transparent;color:inherit;padding:.4rem .65rem;font:inherit;cursor:pointer}.tot-analytics-consent button:disabled{cursor:default;opacity:.45}@media(max-width:34rem){.tot-analytics-consent{left:1rem;right:1rem;align-items:flex-start;flex-direction:column}.tot-analytics-consent__actions{width:100%}.tot-analytics-consent button{flex:1}}`;
212
+ return `<style${nonce ? ` nonce="${nonce}"` : ""}>${style}</style>`;
213
+ }
214
+
215
+ function viewerAnalyticsConsentControl(): string {
216
+ return `<aside class="tot-analytics-consent" data-tot-analytics-consent-control aria-label="Analytics privacy settings">` +
217
+ `<span class="tot-analytics-consent__copy"><strong data-tot-analytics-consent-status aria-live="polite">Analytics off</strong>` +
218
+ `<span class="tot-analytics-consent__detail">Choose whether anonymous analytics may run.</span></span>` +
219
+ `<span class="tot-analytics-consent__actions">` +
220
+ `<button type="button" data-tot-analytics-consent-decision="granted" aria-pressed="false" disabled>Allow</button>` +
221
+ `<button type="button" data-tot-analytics-consent-decision="denied" aria-pressed="true" disabled>Decline</button>` +
222
+ `</span></aside>`;
223
+ }
224
+
199
225
  /**
200
226
  * Replace every {@link INTEGRATION_NONCE_PLACEHOLDER} in `text` (an HTML body or a
201
227
  * CSP header value) with one fresh per-response `nonce`. Idempotent on text with no
@@ -11,13 +11,13 @@
11
11
  * schema versions fail closed.
12
12
  */
13
13
 
14
- export const BROWSER_INTEGRATION_CONTRACT_VERSION = "2.0.0" as const;
14
+ export const BROWSER_INTEGRATION_CONTRACT_VERSION = "3.0.0" as const;
15
15
 
16
16
  export const ENVIRONMENT_SCHEMA_VERSION = 1 as const;
17
17
  export const INTEGRATION_MANIFEST_SCHEMA_VERSION = 2 as const;
18
18
  export const EFFECTIVE_PLAN_SCHEMA_VERSION = 1 as const;
19
19
  export const COMMERCE_EVENT_STOREFRONT_SCHEMA_VERSION = 1 as const;
20
- export const CONSENT_SNAPSHOT_SCHEMA_VERSION = 1 as const;
20
+ export const CONSENT_SNAPSHOT_SCHEMA_VERSION = 2 as const;
21
21
  export const RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION = 1 as const;
22
22
  export const CONSENT_CONFIG_SCHEMA_VERSION = 1 as const;
23
23
  export const ASSURANCE_RECORD_SCHEMA_VERSION = 1 as const;
@@ -21,7 +21,7 @@
21
21
  "expiresAt": "2026-08-28T21:15:30.000Z"
22
22
  },
23
23
  "consent": {
24
- "schemaVersion": 1,
24
+ "schemaVersion": 2,
25
25
  "snapshotId": "consent_public_01J6EXAMPLE",
26
26
  "eligiblePurposes": [
27
27
  "essential",
@@ -20,7 +20,7 @@
20
20
  "expiresAt": "2026-08-28T21:15:30.000Z"
21
21
  },
22
22
  "consent": {
23
- "schemaVersion": 1,
23
+ "schemaVersion": 2,
24
24
  "snapshotId": "consent_public_01J6EXAMPLE",
25
25
  "eligiblePurposes": [
26
26
  "essential",
@@ -11,7 +11,7 @@
11
11
  "supportUrl": "https://support.example.invalid/storefront"
12
12
  },
13
13
  "compatibility": {
14
- "storefrontBrowserContract": ">=2.0.0 <3.0.0",
14
+ "storefrontBrowserContract": ">=3.0.0 <4.0.0",
15
15
  "configurationSchemaVersion": 3
16
16
  },
17
17
  "browser": {
@@ -10,7 +10,7 @@
10
10
  "supportUrl": "https://acme.example/support"
11
11
  },
12
12
  "compatibility": {
13
- "storefrontBrowserContract": ">=2.0.0 <3.0.0",
13
+ "storefrontBrowserContract": ">=3.0.0 <4.0.0",
14
14
  "configurationSchemaVersion": 2
15
15
  },
16
16
  "browser": {
@@ -41,12 +41,6 @@ export const KNOWN_EMBEDS = Object.entries(EMBED_PROVIDERS).map(([slug, p]) => (
41
41
  origins: [...new Set([...(p.scriptSrc ?? []), ...(p.connectSrc ?? []), ...(p.frameSrc ?? [])])],
42
42
  }));
43
43
 
44
- /** Hosts the platform always allows (self is implicit); embeds to these are fine. */
45
- const ALWAYS_ALLOWED_HOSTS = new Set([
46
- "static.cloudflareinsights.com",
47
- "cloudflareinsights.com",
48
- ]);
49
-
50
44
  // Only <script src> and <iframe src> load executable/frameable content the CSP
51
45
  // gates (script-src / frame-src). We deliberately DON'T scan generic URLs
52
46
  // (canonical links, JSON-LD @context, <a href>, og:url) — those aren't embeds
@@ -79,8 +73,6 @@ export function scanHtmlForEmbeds(html) {
79
73
  while ((m = SRC_RE.exec(html))) {
80
74
  const origin = originOf(m[1]);
81
75
  if (!origin) continue;
82
- const host = new URL(origin).hostname;
83
- if (ALWAYS_ALLOWED_HOSTS.has(host)) continue;
84
76
  external.add(origin);
85
77
  }
86
78
  // An external origin is "unknown" if no catalog provider claims it.
@@ -1,19 +0,0 @@
1
- /**
2
- * Reviewed same-origin marker for the native GA4 platform adapter.
3
- *
4
- * The lifecycle host imports this exact-version asset, verifies every field, then
5
- * constructs the adapter from its bundled @tot/public-runtime implementation with
6
- * host-injected fetch. This file executes no vendor code and reads no browser global.
7
- */
8
- export const platformAdapterEntry = Object.freeze({
9
- schemaVersion: 1,
10
- origin: "platform-adapter",
11
- executionTier: "platform-adapter",
12
- appId: "platform.google-measurement",
13
- version: "1.0.0",
14
- entry: Object.freeze({
15
- url: "/platform/google-measurement/v1/entry.mjs",
16
- format: "module",
17
- }),
18
- runtime: "native-ga4-v1",
19
- });