@tokenoftrust/storefront-runner 2.0.40 → 2.0.42

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 (150) hide show
  1. package/apps/storefront/integrations/lifecycle-host-asset.mjs +20 -6
  2. package/apps/storefront/package.json +1 -0
  3. package/apps/storefront/platform-reconcile-tenants.json +3 -0
  4. package/apps/storefront/public/favicon.ico +0 -0
  5. package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +1 -1
  6. package/apps/storefront/public/shared/commerce-chrome.css +147 -16
  7. package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
  8. package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
  9. package/apps/storefront/src/components/Img.astro +6 -5
  10. package/apps/storefront/src/components/ProductCard.astro +6 -4
  11. package/apps/storefront/src/components/Seo.astro +2 -1
  12. package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
  13. package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
  14. package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
  15. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
  16. package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
  17. package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
  18. package/apps/storefront/src/components/home/Hero.astro +156 -4
  19. package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
  20. package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
  21. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
  22. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
  23. package/apps/storefront/src/components/marketing/Faq.astro +9 -2
  24. package/apps/storefront/src/config/devTenantSeed.ts +1 -1
  25. package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
  26. package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
  27. package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
  28. package/apps/storefront/src/config/tenants.ts +22 -0
  29. package/apps/storefront/src/layouts/Layout.astro +69 -44
  30. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
  31. package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
  32. package/apps/storefront/src/lib/asyncCss.ts +23 -0
  33. package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
  34. package/apps/storefront/src/lib/basePath.ts +70 -9
  35. package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
  36. package/apps/storefront/src/lib/chrome/model.ts +4 -0
  37. package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
  38. package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
  39. package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
  40. package/apps/storefront/src/lib/diag/capture.ts +161 -0
  41. package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
  42. package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
  43. package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
  44. package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
  45. package/apps/storefront/src/lib/diag/reference.ts +148 -0
  46. package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
  47. package/apps/storefront/src/lib/diag/report.ts +109 -0
  48. package/apps/storefront/src/lib/diag/session.ts +93 -0
  49. package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
  50. package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
  51. package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
  52. package/apps/storefront/src/lib/imageAttrs.ts +9 -6
  53. package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
  54. package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
  55. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
  56. package/apps/storefront/src/lib/jsonScript.ts +13 -0
  57. package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
  58. package/apps/storefront/src/lib/previewReload.ts +53 -9
  59. package/apps/storefront/src/lib/rawChrome.ts +148 -19
  60. package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
  61. package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
  62. package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
  63. package/apps/storefront/src/lib/tenantRouting.ts +15 -4
  64. package/apps/storefront/src/lib/tot/syncState.ts +21 -0
  65. package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
  66. package/apps/storefront/src/middleware/controlPlane.ts +9 -5
  67. package/apps/storefront/src/middleware/index.ts +13 -1
  68. package/apps/storefront/src/middleware/renderChain.ts +54 -11
  69. package/apps/storefront/src/middleware/routeBranch.ts +58 -12
  70. package/apps/storefront/src/middleware/staticBundle.ts +37 -3
  71. package/apps/storefront/src/pages/[...slug].astro +6 -2
  72. package/apps/storefront/src/pages/api/request-access.ts +5 -4
  73. package/apps/storefront/src/pages/blog/[slug].astro +11 -1
  74. package/apps/storefront/src/pages/cart.ts +23 -0
  75. package/apps/storefront/src/pages/index.astro +64 -85
  76. package/apps/storefront/src/pages/products/[handle].astro +8 -6
  77. package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
  78. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
  79. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
  80. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
  81. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
  82. package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
  83. package/apps/storefront/src/styles/fonts.css +7 -0
  84. package/apps/storefront/src/styles/global.css +397 -3
  85. package/package.json +4 -1
  86. package/packages/cli/src/visual-parity.mjs +321 -0
  87. package/packages/public-runtime/package.json +2 -0
  88. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
  89. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
  90. package/packages/public-runtime/src/chrome.ts +44 -7
  91. package/packages/public-runtime/src/csp.ts +12 -1
  92. package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
  93. package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
  94. package/packages/public-runtime/src/extension-contract.ts +20 -6
  95. package/packages/public-runtime/src/index.ts +8 -0
  96. package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
  97. package/packages/public-runtime/src/integration/consent-config.ts +9 -0
  98. package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
  99. package/packages/public-runtime/src/integration/csp-compiler.ts +6 -3
  100. package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
  101. package/packages/public-runtime/src/integration/index.ts +1 -0
  102. package/packages/public-runtime/src/integration/lifecycle-boot.ts +34 -6
  103. package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
  104. package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
  105. package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
  106. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
  107. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +35 -1
  108. package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +45 -12
  109. package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +28 -7
  110. package/packages/public-runtime/src/integration/native-ga4.ts +18 -2
  111. package/packages/public-runtime/src/integration/platform-adapters.ts +14 -3
  112. package/packages/public-runtime/src/integration/render-phases.ts +28 -2
  113. package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
  114. package/packages/public-runtime/src/publication-redirects.mjs +379 -0
  115. package/packages/public-runtime/src/publication-space.ts +24 -3
  116. package/packages/public-runtime/src/static-bundle.ts +145 -26
  117. package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
  118. package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
  119. package/packages/public-runtime/src/tenant.ts +59 -1
  120. package/pnpm-lock.runner.yaml +12 -0
  121. package/pnpm-workspace.yaml +4 -0
  122. package/scripts/dev/git-status.mjs +1 -1
  123. package/scripts/dev/publish.mjs +6 -0
  124. package/scripts/lib/tenant-asset-copy.mjs +4 -2
  125. package/scripts/tenant/materialize-tenant-content.mjs +1 -0
  126. package/scripts/tenant/validate.mjs +171 -8
  127. package/apps/storefront/src/lib/cfImage.ts +0 -55
  128. package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
  129. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  130. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  131. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  132. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  133. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  134. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  135. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  136. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  137. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  138. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  139. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  140. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  141. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  142. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  143. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  144. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  145. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  146. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  147. package/tenants/home/public/js/storefront.js +0 -129
  148. package/tenants/home/public/logo-wordmark.png +0 -0
  149. package/tenants/home/public/pages/storefront.css +0 -235
  150. package/tenants/home/theme.json +0 -6
@@ -475,10 +475,10 @@ export class ConsentBroker {
475
475
  /**
476
476
  * Viewer-owned analytics consent for the public storefront document.
477
477
  *
478
- * The tenant configuration says which purposes may exist; it never speaks for the
479
- * viewer. This controller therefore removes a baked analytics grant until the viewer
480
- * has made an explicit, unexpired choice. The same synchronous early boot runs in both
481
- * document mounts before any integration or measurement loader can execute.
478
+ * The tenant configuration supplies the analytics default and a stored viewer choice
479
+ * overrides it. Global Privacy Control always denies analytics. The same synchronous
480
+ * early boot runs in both document mounts before any integration or measurement loader
481
+ * can execute.
482
482
  */
483
483
 
484
484
  export const VIEWER_ANALYTICS_CONSENT_STORAGE_KEY = "tot.viewer-analytics-consent.v1";
@@ -492,7 +492,7 @@ export type ViewerAnalyticsConsentDecision = "granted" | "denied";
492
492
  export interface ViewerAnalyticsConsentState {
493
493
  schemaVersion: 1;
494
494
  decision: ViewerAnalyticsConsentDecision;
495
- source: "default-deny" | "stored" | "global-privacy-control";
495
+ source: "tenant-default" | "default-deny" | "stored" | "global-privacy-control";
496
496
  canGrant: boolean;
497
497
  expiresAt: number | null;
498
498
  retentionDays: typeof VIEWER_ANALYTICS_CONSENT_RETENTION_DAYS;
@@ -591,6 +591,7 @@ export function applyEarlyViewerAnalyticsConsent(
591
591
  // approval. Only the current authenticated server-rendered snapshot may grant.
592
592
  const contextComplete = tenantPublicId.length > 0 && environment.length > 0 && siteSurface.length > 0;
593
593
  const tenantApprovedAnalytics = contextComplete && rawApproved.includes("analytics");
594
+ const tenantDefaultAnalytics = tenantApprovedAnalytics && rawEligible.includes("analytics");
594
595
  const globalPrivacyControl = win.navigator?.globalPrivacyControl === true;
595
596
 
596
597
  let stored: { decision: ViewerAnalyticsConsentDecision; expiresAt: number } | null = null;
@@ -631,12 +632,14 @@ export function applyEarlyViewerAnalyticsConsent(
631
632
  const decision: ViewerAnalyticsConsentDecision =
632
633
  globalPrivacyControl || !tenantApprovedAnalytics
633
634
  ? "denied"
634
- : stored?.decision ?? "denied";
635
+ : stored?.decision ?? (tenantDefaultAnalytics ? "granted" : "denied");
635
636
  const source: ViewerAnalyticsConsentState["source"] = globalPrivacyControl
636
637
  ? "global-privacy-control"
637
638
  : stored
638
639
  ? "stored"
639
- : "default-deny";
640
+ : tenantDefaultAnalytics
641
+ ? "tenant-default"
642
+ : "default-deny";
640
643
 
641
644
  const eligible = canonicalPurposes.filter(
642
645
  (purpose) =>
@@ -258,3 +258,12 @@ export function unmetConsentDependencies(
258
258
  export function emptyConsentConfig(): TenantConsentConfig {
259
259
  return { schemaVersion: CONSENT_CONFIG_SCHEMA_VERSION, purposes: [], cmp: { mode: "none" } };
260
260
  }
261
+
262
+ /** Platform default when a tenant configured analytics and saved no consent override. */
263
+ export function defaultAnalyticsConsentConfig(): TenantConsentConfig {
264
+ return {
265
+ schemaVersion: CONSENT_CONFIG_SCHEMA_VERSION,
266
+ purposes: [{ purpose: "analytics", legalBasis: "legitimate-interest", defaultState: "granted" }],
267
+ cmp: { mode: "none" },
268
+ };
269
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The platform-owned browser binding for the two reviewed contact-sales CTAs.
2
+ * The platform-owned browser binding for reviewed contact-sales CTA identities.
3
3
  *
4
4
  * This is deliberately not a general interaction adapter. Both DOM identifiers and
5
5
  * the emitted semantic name are fixed below; the source reads no labels, URLs,
@@ -10,19 +10,41 @@ import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety
10
10
 
11
11
  /** The only reviewed CTA identities that may raise this marketing action. */
12
12
  export const CONTACT_SALES_ACTION_SOURCES = [
13
- { ctaId: "cta-book-demo", logicalId: "contact-sales-book-demo" },
14
- { ctaId: "contact-sales", logicalId: "contact-sales-lower" },
13
+ { selector: '[data-cta-id="cta-book-demo"]', logicalId: "contact-sales-book-demo" },
14
+ { selector: '[data-cta-id="book-demo"]', logicalId: "contact-sales-book-demo" },
15
+ { selector: '[data-cta-id="contact-sales"]', logicalId: "contact-sales-lower" },
16
+ { selector: '[data-tot-action="contact-sales"]', logicalId: "contact-sales-action" },
17
+ { selector: '[data-tot-action="nav-contact-sales"]', logicalId: "contact-sales-nav" },
15
18
  ] as const;
16
19
 
17
- const CONTACT_SALES_ACTION_NAME = "user_clicked_contact_sales_mktg" as const;
20
+ export const CONTACT_SALES_ACTION_NAME = "user_clicked_contact_sales_mktg" as const;
21
+ /** The reviewed action allowlist paired with this fixed platform source. */
22
+ export const CONTACT_SALES_ACTION_REGISTRY = {
23
+ schemaVersion: 1,
24
+ actions: [{ name: CONTACT_SALES_ACTION_NAME }],
25
+ } as const;
18
26
  const CORRELATION_TTL_MS = 60 * 60 * 1000;
27
+ export const CONTACT_SALES_NAVIGATION_DEFER_MS = 100;
28
+
29
+ export interface ContactSalesClickEvent {
30
+ altKey?: boolean;
31
+ button?: number;
32
+ ctrlKey?: boolean;
33
+ defaultPrevented?: boolean;
34
+ metaKey?: boolean;
35
+ preventDefault?(): void;
36
+ shiftKey?: boolean;
37
+ }
19
38
 
20
39
  export interface ContactSalesActionElement {
21
- addEventListener(type: "click", listener: () => void): void;
22
- removeEventListener(type: "click", listener: () => void): void;
40
+ download?: string;
41
+ href?: string;
42
+ target?: string;
43
+ addEventListener(type: "click", listener: (event: ContactSalesClickEvent) => void): void;
44
+ removeEventListener(type: "click", listener: (event: ContactSalesClickEvent) => void): void;
23
45
  }
24
46
 
25
- /** The narrow DOM seam used only to find the two fixed platform CTA identities. */
47
+ /** The narrow DOM seam used only to find the fixed platform CTA identities. */
26
48
  export interface ContactSalesActionDocument {
27
49
  querySelectorAll(selector: string): ArrayLike<ContactSalesActionElement>;
28
50
  }
@@ -31,10 +53,23 @@ export interface ContactSalesActionSourceOptions {
31
53
  document?: ContactSalesActionDocument;
32
54
  planRevision: string;
33
55
  now?: () => number;
56
+ navigate?: (href: string) => void;
57
+ schedule?: (callback: () => void, delayMs: number) => void;
58
+ }
59
+
60
+ function deferredNavigationHref(node: ContactSalesActionElement, event: ContactSalesClickEvent): string | undefined {
61
+ if (
62
+ event.defaultPrevented || (event.button !== undefined && event.button !== 0) ||
63
+ event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
64
+ typeof event.preventDefault !== "function" || typeof node.href !== "string" || !node.href ||
65
+ (typeof node.target === "string" && node.target !== "" && node.target !== "_self") ||
66
+ (typeof node.download === "string" && node.download !== "")
67
+ ) return undefined;
68
+ return node.href;
34
69
  }
35
70
 
36
71
  /**
37
- * Bind literal `data-cta-id` values to a typed source for the managed-analytics
72
+ * Bind the fixed reviewed DOM identities to a typed source for the managed-analytics
38
73
  * bridge. A keyboard activation of an anchor/button produces its native `click`, so
39
74
  * it follows this same semantic path without a separate key listener.
40
75
  */
@@ -43,6 +78,7 @@ export function createContactSalesActionSource(
43
78
  ): PlatformManagedActionSource {
44
79
  const document = options.document;
45
80
  const now = options.now ?? Date.now;
81
+ const schedule = options.schedule ?? ((callback, delayMs) => { setTimeout(callback, delayMs); });
46
82
  const revisionId = managedAnalyticsOpaqueRevision(options.planRevision);
47
83
  let sequence = 0;
48
84
 
@@ -50,10 +86,15 @@ export function createContactSalesActionSource(
50
86
  subscribe(listener) {
51
87
  if (!document || typeof document.querySelectorAll !== "function") return () => {};
52
88
  const removals: Array<() => void> = [];
89
+ const bound = new Set<ContactSalesActionElement>();
53
90
  for (const source of CONTACT_SALES_ACTION_SOURCES) {
54
- const nodes = document.querySelectorAll(`[data-cta-id="${source.ctaId}"]`);
91
+ const nodes = document.querySelectorAll(source.selector);
55
92
  for (const node of Array.from(nodes)) {
56
- const emit = (): void => {
93
+ if (bound.has(node)) continue;
94
+ bound.add(node);
95
+ const emit = (event: ContactSalesClickEvent = {}): void => {
96
+ const navigationHref = options.navigate ? deferredNavigationHref(node, event) : undefined;
97
+ if (navigationHref) event.preventDefault?.();
57
98
  sequence += 1;
58
99
  const at = now();
59
100
  const activation: PlatformManagedActionActivation = {
@@ -66,6 +107,11 @@ export function createContactSalesActionSource(
66
107
  sequence,
67
108
  };
68
109
  listener(activation);
110
+ // A full-document navigation destroys the isolated GA4 iframe. Give its
111
+ // MessagePort one bounded turn to receive the reviewed action first.
112
+ if (navigationHref) {
113
+ schedule(() => options.navigate?.(navigationHref), CONTACT_SALES_NAVIGATION_DEFER_MS);
114
+ }
69
115
  };
70
116
  node.addEventListener("click", emit);
71
117
  removals.push(() => node.removeEventListener("click", emit));
@@ -63,9 +63,12 @@ export const INTEGRATION_CSP_PROFILES: Readonly<Record<string, CspSourceSet>> =
63
63
  "connect-src": ["https://cdn.usefathom.com"],
64
64
  },
65
65
 
66
- // Google code and network authority live only in the null-origin sandbox. The
67
- // parent needs no addition: base frame-src 'self' admits the versioned host.
68
- "google-measurement": {},
66
+ // Google code and network authority live only in the isolated-origin sandbox. The
67
+ // hosted preview uses the dedicated tokenoftrust.store sibling. Custom-domain
68
+ // publication aliases are added from the validated canonical host at declaration time.
69
+ "google-measurement": {
70
+ "frame-src": ["https://analytics-frame.tokenoftrust.store"],
71
+ },
69
72
 
70
73
  // Meta pixel (fbevents.js). The loader is served from connect.facebook.net; the
71
74
  // pixel's collect endpoint is the facebook.com tracking origin. The tenant
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The governed event-property set every reviewed analytics adapter attaches to
3
+ * every event it sends.
4
+ *
5
+ * These three values answer "whose traffic, on which surface, from which stage"
6
+ * — the question a merchant has to answer before any storefront number is worth
7
+ * reading. They are DERIVED from the renderer's own trusted context
8
+ * (`BrowserEnvironment` + the server-stamped tenant identity), never declared by
9
+ * a tenant and never read from the browser. That is the whole point: a tenant
10
+ * that could type its own `tenant` or `environment` could mislabel its traffic,
11
+ * so the adapter configuration contract bans those keys outright
12
+ * (`managed-analytics-api-v1.schemas.ts` FORBIDDEN_KEY_PATTERN, `secrets.ts`
13
+ * findForbiddenFields) and this module supplies them instead.
14
+ *
15
+ * They carry no person, device, account, or network signal, so they add nothing
16
+ * to the anonymous-collection posture the adapters already hold.
17
+ */
18
+ import {
19
+ validateBrowserEnvironment,
20
+ type BrowserEnvironment,
21
+ type IntegrationEnvironment,
22
+ type IntegrationSiteSurface,
23
+ } from "./environment.js";
24
+
25
+ /** The exact, closed key set. Adding a key here is a reviewed platform change. */
26
+ export const GOVERNED_EVENT_PROPERTY_KEYS = Object.freeze([
27
+ "tenant",
28
+ "environment",
29
+ "site_surface",
30
+ ] as const);
31
+
32
+ export interface GovernedEventProperties {
33
+ /** The tenant's public id, e.g. `tokenoftrust.com`. Server-stamped. */
34
+ tenant: string;
35
+ /** The four-stage publication stage: `local` | `candidate` | `staged` | `live`. */
36
+ environment: IntegrationEnvironment;
37
+ /** The document surface: `storefront` | `checkout` | `admin` | `marketing`. */
38
+ site_surface: IntegrationSiteSurface;
39
+ }
40
+
41
+ /**
42
+ * Build the governed property set, or null when the context cannot be trusted.
43
+ *
44
+ * Fail-closed is deliberate and matches every other adapter gate: an adapter
45
+ * that cannot prove which tenant and stage an event belongs to must not label
46
+ * it, and a caller that gets null drops the event rather than sending it
47
+ * unlabelled. Mislabelled traffic is worse than absent traffic — it silently
48
+ * corrupts every report built on it.
49
+ */
50
+ export function buildGovernedEventProperties(
51
+ environment: BrowserEnvironment,
52
+ tenantPublicId: string,
53
+ ): GovernedEventProperties | null {
54
+ const validated = validateBrowserEnvironment(environment);
55
+ if (!validated.ok) return null;
56
+ if (typeof tenantPublicId !== "string" || tenantPublicId.length === 0) return null;
57
+ return Object.freeze({
58
+ tenant: tenantPublicId,
59
+ environment: validated.value.environment,
60
+ site_surface: validated.value.siteSurface,
61
+ });
62
+ }
@@ -59,6 +59,7 @@ export * from "./platform-adapters.js";
59
59
  export * from "./native-amplitude.js";
60
60
  export * from "./native-ga4.js";
61
61
  export * from "./native-ga4-sandbox.js";
62
+ export * from "./native-ga4-sandbox-asset.mjs";
62
63
  export * from "./platform-adapter-runtime.js";
63
64
  export * from "./render-phases.js";
64
65
  export * from "./interim-bridge.js";
@@ -16,7 +16,7 @@
16
16
  * constructs bundled platform code with host-owned capabilities. Every other entry goes
17
17
  * through the authority-tier loader: it loads ONLY an `elevated-parent` entry (whose
18
18
  * honest, review-only contract IS arbitrary parent-document JavaScript) and REFUSES every
19
- * other tier. A reviewed registry entry may construct its own null-origin iframe and
19
+ * other tier. A reviewed registry entry may construct its own isolated-origin iframe and
20
20
  * private typed MessagePort; arbitrary `sandbox`-tier entry modules remain refused by
21
21
  * the parent-realm loader, so their isolation is never silently downgraded. Loading is
22
22
  * also consent-gated: the binding reads
@@ -42,7 +42,7 @@ import type { NavigationCompletedEvent, NavigationKind } from "./events.js";
42
42
  import { CommerceEventBroker } from "./commerce-broker.js";
43
43
  import { validateBrowserEnvironment, BROWSER_ENVIRONMENT_GLOBAL, type BrowserEnvironment } from "./environment.js";
44
44
  import { makePlatformAdapterRuntimeLoader } from "./platform-adapter-runtime.js";
45
- import { NATIVE_GA4_SANDBOX_URL, type NativeGa4Transport } from "./native-ga4.js";
45
+ import { resolveNativeGa4SandboxUrl, type NativeGa4Transport } from "./native-ga4.js";
46
46
  import {
47
47
  createBrowserNativeGa4SandboxHost,
48
48
  createNativeGa4SandboxTransport,
@@ -51,6 +51,7 @@ import { ManagedAnalyticsEventBrokerBridge } from "./managed-analytics-bridge.js
51
51
  import { minimizePageAttribution, type ManagedPageViewInput } from "./managed-analytics.js";
52
52
  import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety.js";
53
53
  import {
54
+ CONTACT_SALES_ACTION_REGISTRY,
54
55
  createContactSalesActionSource,
55
56
  type ContactSalesActionDocument,
56
57
  } from "./contact-sales-action-source.js";
@@ -68,7 +69,7 @@ export const LIFECYCLE_NAVIGATION_SINK_GLOBAL = "__TOT_INTEGRATION_NAVIGATION__"
68
69
 
69
70
  /** The narrow slice of `window` the binding depends on — structural, for edge-safe typing. */
70
71
  export interface LifecycleWindow {
71
- location: { pathname: string; search: string; hash: string; href: string };
72
+ location: { pathname: string; search: string; hash: string; href: string; assign?: (href: string) => void };
72
73
  addEventListener(type: string, listener: (event: unknown) => void): void;
73
74
  fetch?: typeof fetch;
74
75
  /**
@@ -118,7 +119,7 @@ export interface BootLifecycleOptions {
118
119
  fetchCapability?: typeof fetch;
119
120
  /** Entry import capability; injectable for tests and non-browser hosts. */
120
121
  importModule?: (url: string) => Promise<unknown>;
121
- /** Reviewed null-origin GA4 transport; injectable for DOM-free tests. */
122
+ /** Reviewed isolated-origin GA4 transport; injectable for DOM-free tests. */
122
123
  nativeGa4Transport?: NativeGa4Transport;
123
124
  }
124
125
 
@@ -207,6 +208,7 @@ function makeDocumentCommerceBroker(
207
208
  },
208
209
  source: `//${descriptor.tenantPublicId}`,
209
210
  consentSnapshot: getConsentSnapshot,
211
+ tenantActionRegistry: CONTACT_SALES_ACTION_REGISTRY,
210
212
  });
211
213
  }
212
214
 
@@ -242,7 +244,7 @@ function documentPageView(
242
244
  const id = `pageview-${managedAnalyticsOpaqueRevision(descriptor.planRevision)}`;
243
245
  return {
244
246
  logicalId: id,
245
- path: win.location.pathname,
247
+ path: canonicalDocumentPath(win.location.pathname, descriptor.tenantPublicId),
246
248
  correlation: {
247
249
  id,
248
250
  expiresAt: new Date(now() + PAGE_VIEW_CORRELATION_TTL_MS).toISOString(),
@@ -251,6 +253,28 @@ function documentPageView(
251
253
  };
252
254
  }
253
255
 
256
+ /**
257
+ * Project immutable preview wrapper routes back to the tenant path they render.
258
+ * The wrapper contains a commit SHA, which is both non-canonical and deliberately
259
+ * rejected by the analytics value-safety boundary. Public/live routes pass through.
260
+ */
261
+ export function canonicalDocumentPath(pathname: string, tenantPublicId: string): string {
262
+ const segments = pathname.split("/");
263
+ const previewIndex = segments[1] === tenantPublicId && segments[2] === "preview"
264
+ ? 2
265
+ : segments[1] === "preview"
266
+ ? 1
267
+ : -1;
268
+ if (
269
+ previewIndex !== -1 &&
270
+ (segments[previewIndex + 1] === "staged" || segments[previewIndex + 1] === "rev") &&
271
+ /^[0-9a-f]{40,64}$/i.test(segments[previewIndex + 2] ?? "")
272
+ ) {
273
+ return `/${segments.slice(previewIndex + 3).join("/")}`;
274
+ }
275
+ return pathname;
276
+ }
277
+
254
278
  function readDescriptor(win: LifecycleWindow): IntegrationBootDescriptor | null {
255
279
  const descriptor = win[INTEGRATION_PLAN_GLOBAL];
256
280
  if (!descriptor || typeof descriptor !== "object") return null;
@@ -326,7 +350,7 @@ export function bootIntegrationLifecycle(
326
350
  }
327
351
  return createNativeGa4SandboxTransport(
328
352
  createBrowserNativeGa4SandboxHost(document, MessageChannel),
329
- NATIVE_GA4_SANDBOX_URL,
353
+ resolveNativeGa4SandboxUrl(win.location.href),
330
354
  );
331
355
  },
332
356
  });
@@ -364,6 +388,10 @@ export function bootIntegrationLifecycle(
364
388
  : undefined,
365
389
  planRevision: descriptor.planRevision,
366
390
  now: options.now ?? Date.now,
391
+ navigate: (href) => {
392
+ if (typeof win.location.assign === "function") win.location.assign(href);
393
+ else win.location.href = href;
394
+ },
367
395
  }),
368
396
  }),
369
397
  pageView: () => documentPageView(win, descriptor, options.now ?? Date.now),
@@ -1,3 +1,5 @@
1
1
  /** Types for the dual-consumer (`.ts` engine + `.mjs` build plugin) host-asset path. */
2
2
  export declare const LIFECYCLE_HOST_ASSET_PATH: string;
3
3
  export declare const LIFECYCLE_HOST_ASSET_URL: string;
4
+ export declare const LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS: readonly string[];
5
+ export declare const LIFECYCLE_HOST_COMPATIBILITY_ASSET_URLS: readonly string[];
@@ -3,15 +3,29 @@
3
3
  *
4
4
  * It lives in a `.mjs` module because it has TWO consumers that cannot share a `.ts`
5
5
  * one: the render engine (`render-phases.ts`) that emits the `<script src>`, and the
6
- * build plugin (`apps/storefront/build-plugins/lifecycle-host-asset.mjs`) that produces
6
+ * build integration (`apps/storefront/integrations/lifecycle-host-asset.mjs`) that produces
7
7
  * the file at that exact path. One source of truth, so the tag and the artifact cannot
8
8
  * disagree — a mismatch would leave every static tenant page with plan data and no host,
9
9
  * which fails silently in exactly the way it is not allowed to.
10
10
  *
11
- * The path is version-pinned (`/v1/`) like a platform adapter entry, so a future host
11
+ * The path is version-pinned (`/v3/`) like a platform adapter entry, so a future host
12
12
  * with a different boot contract ships alongside rather than replacing this one.
13
13
  */
14
- export const LIFECYCLE_HOST_ASSET_PATH = "platform/lifecycle/v1/boot.mjs";
14
+ export const LIFECYCLE_HOST_ASSET_PATH = "platform/lifecycle/v3/boot.mjs";
15
+
16
+ /**
17
+ * Earlier immutable URLs that must keep serving the current host bytes during a
18
+ * version transition. Cached HTML may still reference one of these paths after a
19
+ * pointer flip, so dropping it would turn a cache refresh into a lifecycle 404.
20
+ */
21
+ export const LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS = Object.freeze([
22
+ "platform/lifecycle/v2/boot.mjs",
23
+ ]);
15
24
 
16
25
  /** The root-absolute, same-origin URL the served markup references. */
17
26
  export const LIFECYCLE_HOST_ASSET_URL = `/${LIFECYCLE_HOST_ASSET_PATH}`;
27
+
28
+ /** Root-absolute compatibility URLs emitted alongside the current URL. */
29
+ export const LIFECYCLE_HOST_COMPATIBILITY_ASSET_URLS = Object.freeze(
30
+ LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS.map((path) => `/${path}`),
31
+ );
@@ -20,6 +20,10 @@ import {
20
20
  type IntegrationEnvironment,
21
21
  } from "./environment.js";
22
22
  import { resemblesManagedAnalyticsPii } from "./managed-analytics-value-safety.js";
23
+ import {
24
+ buildGovernedEventProperties,
25
+ type GovernedEventProperties,
26
+ } from "./governed-event-properties.js";
23
27
  import type {
24
28
  IntegrationEventSubscribe,
25
29
  IntegrationLifecycleContext,
@@ -47,7 +51,7 @@ export interface AmplitudeHttpEvent {
47
51
  event_type: "page_view" | (typeof APPROVED_MARKETING_ACTION_NAMES)[number];
48
52
  device_id: typeof ANONYMOUS_DEVICE_ID;
49
53
  insert_id: string;
50
- event_properties: { page_path: string } | Record<string, never>;
54
+ event_properties: GovernedEventProperties & { page_path?: string };
51
55
  }
52
56
 
53
57
  export const AMPLITUDE_DEDUPE_CAPACITY = 512 as const;
@@ -127,6 +131,11 @@ export function mapCommerceEventToAmplitude(
127
131
  ) return null;
128
132
  if (!event.consent.eligiblePurposes.includes("analytics")) return null;
129
133
 
134
+ // Derived from the same trusted context the equality check above just proved
135
+ // this event agrees with, so labelling can never disagree with admission.
136
+ const governed = buildGovernedEventProperties(environmentResult.value, tenantPublicId);
137
+ if (!governed) return null;
138
+
130
139
  const base = {
131
140
  device_id: ANONYMOUS_DEVICE_ID,
132
141
  insert_id: eventDedupeKey(event),
@@ -137,12 +146,12 @@ export function mapCommerceEventToAmplitude(
137
146
  return {
138
147
  ...base,
139
148
  event_type: name as (typeof APPROVED_MARKETING_ACTION_NAMES)[number],
140
- event_properties: {},
149
+ event_properties: { ...governed },
141
150
  };
142
151
  }
143
152
  const path = event.data["path"];
144
153
  if (!isSafePagePath(path)) return null;
145
- return { ...base, event_type: "page_view", event_properties: { page_path: path } };
154
+ return { ...base, event_type: "page_view", event_properties: { ...governed, page_path: path } };
146
155
  }
147
156
 
148
157
  export interface RegisterAmplitudeHttpDestinationInput {
@@ -1,2 +1,4 @@
1
1
  export const NATIVE_GA4_SANDBOX_ASSET_PATH: "platform/google-measurement/v2/sandbox.html";
2
2
  export const NATIVE_GA4_SANDBOX_ASSET_URL: "/platform/google-measurement/v2/sandbox.html";
3
+ export const NATIVE_GA4_SANDBOX_ORIGIN: "https://analytics-frame.tokenoftrust.store";
4
+ export function resolveNativeGa4SandboxUrl(parentHref: string): string;
@@ -1,3 +1,37 @@
1
- /** Stable provider-neutral artifact path for the reviewed GA4 null-origin sandbox. */
1
+ /** Stable provider-neutral artifact path for the reviewed GA4 transport frame. */
2
2
  export const NATIVE_GA4_SANDBOX_ASSET_PATH = "platform/google-measurement/v2/sandbox.html";
3
3
  export const NATIVE_GA4_SANDBOX_ASSET_URL = `/${NATIVE_GA4_SANDBOX_ASSET_PATH}`;
4
+ export const NATIVE_GA4_SANDBOX_ORIGIN = "https://analytics-frame.tokenoftrust.store";
5
+
6
+ /**
7
+ * Keep Google tag execution on a real, isolated origin. An opaque sandbox origin makes
8
+ * gtag.js load successfully but silently suppresses every collection request. Production
9
+ * uses a dedicated no-auth hostname for hosted surfaces and the tenant's paired apex/www
10
+ * publication alias for custom domains. Local development uses an analytics.localhost
11
+ * sibling while retaining the current dev-server port.
12
+ */
13
+ export function resolveNativeGa4SandboxUrl(parentHref) {
14
+ const parent = new URL(parentHref);
15
+ if (parent.hostname === "localhost" || parent.hostname.endsWith(".localhost")) {
16
+ const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
17
+ sandbox.hostname = "analytics.localhost";
18
+ return sandbox.href;
19
+ }
20
+ if (parent.hostname === "tokenoftrust.store" || parent.hostname.endsWith(".tokenoftrust.store")) {
21
+ return new URL(NATIVE_GA4_SANDBOX_ASSET_URL, NATIVE_GA4_SANDBOX_ORIGIN).href;
22
+ }
23
+ // A static publication verification alias is deliberately not the tenant's
24
+ // canonical apex/www pair. Its frame must still share the tenant's site: gtag.js
25
+ // loads but suppresses collection from a third-party frame. The corresponding
26
+ // analytics-frame hostname is provisioned as a Cloudflare-for-SaaS custom host.
27
+ if (parent.hostname.startsWith("published.")) {
28
+ const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
29
+ sandbox.hostname = `analytics-frame.${parent.hostname.slice("published.".length)}`;
30
+ return sandbox.href;
31
+ }
32
+ const sandbox = new URL(NATIVE_GA4_SANDBOX_ASSET_URL, parent);
33
+ sandbox.hostname = parent.hostname.startsWith("www.")
34
+ ? parent.hostname.slice("www.".length)
35
+ : `www.${parent.hostname}`;
36
+ return sandbox.href;
37
+ }
@@ -9,6 +9,7 @@ import { resemblesManagedAnalyticsProtectedValue } from "./managed-analytics-val
9
9
 
10
10
  const MEASUREMENT_ID = /^G-[A-Z0-9]{4,20}$/;
11
11
  const CAMPAIGN_VALUE = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/;
12
+ const GOVERNED_VALUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
13
  const APPROVED_ACTIONS: ReadonlySet<string> = new Set(APPROVED_MARKETING_ACTION_NAMES);
13
14
 
14
15
  function plainRecord(value: unknown): Record<string, unknown> | null {
@@ -44,6 +45,20 @@ function validIds(value: unknown): value is string[] {
44
45
  value.every((id) => typeof id === "string" && MEASUREMENT_ID.test(id)) && new Set(value).size === value.length;
45
46
  }
46
47
 
48
+ /**
49
+ * Governed labelling arrives across a hostile boundary, so it is re-validated
50
+ * here rather than trusted: the exact three keys, each a bare token. `tenant`
51
+ * is a public host id and the other two are closed enums the parent derives, so
52
+ * nothing here can smuggle a value gtag would treat as anything but a label.
53
+ */
54
+ function validGovernedProperties(value: unknown): value is Record<string, string> {
55
+ const record = plainRecord(value);
56
+ if (!record || !exactKeys(record, ["tenant", "environment", "site_surface"])) return false;
57
+ return Object.values(record).every((item) =>
58
+ typeof item === "string" && GOVERNED_VALUE.test(item) &&
59
+ !resemblesManagedAnalyticsProtectedValue(item));
60
+ }
61
+
47
62
  function validCampaign(value: unknown): value is Record<string, string> {
48
63
  const record = plainRecord(value);
49
64
  if (!record || !exactKeys(record, ["source", "medium", "campaign", "term", "content"], [])) return false;
@@ -64,11 +79,11 @@ export function startNativeGa4Sandbox(win: NativeGa4SandboxRuntimeWindow, doc: D
64
79
  const bind = (event: MessageEvent): void => {
65
80
  try {
66
81
  const data = plainRecord(event.data);
67
- const ownOrigin = new URL(win.location.href).origin;
68
82
  if (
69
- bound || event.source !== win.parent || event.origin !== ownOrigin || event.ports.length !== 1 ||
70
- !data || !exactKeys(data, ["protocol", "type"]) ||
83
+ bound || event.source !== win.parent || event.ports.length !== 1 ||
84
+ !data || !exactKeys(data, ["protocol", "type", "parentOrigin"]) ||
71
85
  data["protocol"] !== NATIVE_GA4_SANDBOX_PROTOCOL || data["type"] !== "bind"
86
+ || !validOrigin(data["parentOrigin"]) || event.origin !== data["parentOrigin"]
72
87
  ) return;
73
88
  bound = true;
74
89
  win.removeEventListener("message", bind);
@@ -85,9 +100,9 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
85
100
  let active = false;
86
101
  let disposed = false;
87
102
  let canonicalOrigin = "";
88
- let currentGroup = "";
103
+ let currentDestinations: string[] = [];
104
+ let governedProperties: Record<string, string> = {};
89
105
  let lastPageLocation = "";
90
- let generation = 0;
91
106
  let tagLoaded = false;
92
107
 
93
108
  const dataLayer: unknown[] = [];
@@ -99,13 +114,16 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
99
114
 
100
115
  const configure = (message: Record<string, unknown>): void => {
101
116
  if (
102
- !exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin"]) ||
117
+ !exactKeys(message, ["schemaVersion", "type", "destinationIds", "canonicalOrigin", "analyticsConsent", "governedProperties"]) ||
103
118
  message["schemaVersion"] !== 1 || message["type"] !== "configure" ||
104
- !validIds(message["destinationIds"]) || !validOrigin(message["canonicalOrigin"])
119
+ message["analyticsConsent"] !== "granted" ||
120
+ !validIds(message["destinationIds"]) || !validOrigin(message["canonicalOrigin"]) ||
121
+ !validGovernedProperties(message["governedProperties"])
105
122
  ) return;
106
123
  const ids = message["destinationIds"];
107
124
  canonicalOrigin = message["canonicalOrigin"];
108
- currentGroup = `tot_storefront_${++generation}`;
125
+ governedProperties = { ...message["governedProperties"] };
126
+ currentDestinations = [...ids];
109
127
  lastPageLocation = "";
110
128
  active = true;
111
129
 
@@ -119,9 +137,13 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
119
137
  });
120
138
  gtag("js", new Date());
121
139
  }
140
+ // This sandbox is created and configured only after the parent lifecycle's
141
+ // platform consent gate grants analytics. Mirror that granted state into Google's
142
+ // consent mode; leaving its default denied suppresses the explicit page_view that
143
+ // follows even though the platform correctly admitted it.
144
+ gtag("consent", "update", { analytics_storage: message["analyticsConsent"] });
122
145
  for (const id of ids) {
123
146
  gtag("config", id, {
124
- groups: currentGroup,
125
147
  send_page_view: false,
126
148
  allow_google_signals: false,
127
149
  allow_ad_personalization_signals: false,
@@ -148,7 +170,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
148
170
  const pageLocation = `${canonicalOrigin}${message["path"]}`;
149
171
  lastPageLocation = pageLocation;
150
172
  const params: Record<string, unknown> = {
151
- send_to: currentGroup,
173
+ ...governedProperties,
152
174
  page_location: pageLocation,
153
175
  page_path: message["path"],
154
176
  };
@@ -160,7 +182,9 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
160
182
  if (campaign[key] !== undefined) params[`campaign_${ga4Key}`] = campaign[key];
161
183
  }
162
184
  }
163
- gtag("event", "page_view", params);
185
+ for (const id of currentDestinations) {
186
+ gtag("event", "page_view", { ...params, send_to: id });
187
+ }
164
188
  return;
165
189
  }
166
190
  if (message["type"] === "action") {
@@ -169,7 +193,14 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
169
193
  typeof message["name"] !== "string" || !APPROVED_ACTIONS.has(message["name"]) ||
170
194
  message["pageLocation"] !== lastPageLocation || !lastPageLocation
171
195
  ) return;
172
- gtag("event", message["name"], { send_to: currentGroup, page_location: lastPageLocation });
196
+ for (const id of currentDestinations) {
197
+ gtag("event", message["name"], {
198
+ ...governedProperties,
199
+ send_to: id,
200
+ page_location: lastPageLocation,
201
+ transport_type: "beacon",
202
+ });
203
+ }
173
204
  }
174
205
  };
175
206
 
@@ -181,6 +212,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
181
212
  if (message["type"] === "disable") {
182
213
  if (exactKeys(message, ["schemaVersion", "type"]) && message["schemaVersion"] === 1) {
183
214
  active = false;
215
+ currentDestinations = [];
184
216
  lastPageLocation = "";
185
217
  }
186
218
  return;
@@ -189,6 +221,7 @@ function runPort(port: MessagePort, win: NativeGa4SandboxRuntimeWindow, doc: Doc
189
221
  if (exactKeys(message, ["schemaVersion", "type"]) && message["schemaVersion"] === 1) {
190
222
  active = false;
191
223
  disposed = true;
224
+ currentDestinations = [];
192
225
  lastPageLocation = "";
193
226
  port.close();
194
227
  }