@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.
- package/apps/storefront/astro.config.mjs +2 -0
- package/apps/storefront/env.d.ts +0 -2
- package/apps/storefront/integrations/google-measurement-sandbox-asset.mjs +97 -0
- package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +13 -0
- package/apps/storefront/src/layouts/Layout.astro +3 -48
- package/apps/storefront/src/lib/analytics/budgets.json +0 -1
- package/apps/storefront/src/lib/analytics/index.ts +1 -4
- package/apps/storefront/src/lib/analytics/webVitals.ts +42 -4
- package/apps/storefront/src/lib/integration/tenantDeclarations.ts +1 -1
- package/apps/storefront/src/lib/monitoring/manifest.ts +1 -2
- package/package.json +1 -1
- package/packages/public-runtime/package.json +2 -1
- package/packages/public-runtime/src/csp.ts +2 -2
- package/packages/public-runtime/src/integration/consent-broker.ts +370 -13
- package/packages/public-runtime/src/integration/csp-compiler.ts +3 -6
- package/packages/public-runtime/src/integration/events.ts +5 -5
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/interaction-tracking-gate.ts +1 -2
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +28 -14
- package/packages/public-runtime/src/integration/lifecycle-host-entry.ts +2 -0
- package/packages/public-runtime/src/integration/lifecycle-runtime.ts +256 -22
- package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +9 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +3 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +207 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +168 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +111 -215
- package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +18 -9
- package/packages/public-runtime/src/integration/platform-adapters.ts +20 -9
- package/packages/public-runtime/src/integration/render-phases.ts +28 -2
- package/packages/public-runtime/src/integration/version.ts +2 -2
- package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +1 -1
- package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +1 -1
- package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +1 -1
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +1 -1
- package/scripts/tenant/lint-embeds.mjs +0 -8
- 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
|
|
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: ({
|
|
74
|
-
|
|
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
|
-
|
|
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({
|
|
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.
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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/
|
|
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:
|
|
164
|
+
maxAdditionalRequests: 5,
|
|
158
165
|
maxMainThreadMilliseconds: 120,
|
|
159
166
|
mustBeAsyncOrDeferred: true,
|
|
160
167
|
}),
|
|
161
168
|
idLabel: "GA4 measurement id (e.g. G-XXXXXXX)",
|
|
162
|
-
//
|
|
163
|
-
//
|
|
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") +
|
|
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 = "
|
|
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 =
|
|
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;
|
|
@@ -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
|
-
});
|