@tokenoftrust/storefront-runner 2.0.3 → 2.0.5

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 (178) hide show
  1. package/apps/storefront/.astro/content.d.ts +159 -0
  2. package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
  3. package/apps/storefront/.astro/types.d.ts +3 -0
  4. package/apps/storefront/migrations-apps/0011_dt_tracking_tenant_databases.sql +23 -0
  5. package/apps/storefront/node_modules/.bin/acorn +43 -0
  6. package/apps/storefront/node_modules/.bin/astro +43 -0
  7. package/apps/storefront/node_modules/.bin/astro-check +43 -0
  8. package/apps/storefront/node_modules/.bin/browserslist +43 -0
  9. package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
  10. package/apps/storefront/node_modules/.bin/depcruise +43 -0
  11. package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
  12. package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
  13. package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
  14. package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
  15. package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
  16. package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
  17. package/apps/storefront/node_modules/.bin/esbuild +43 -0
  18. package/apps/storefront/node_modules/.bin/jiti +43 -0
  19. package/apps/storefront/node_modules/.bin/playwright +43 -0
  20. package/apps/storefront/node_modules/.bin/playwright-core +43 -0
  21. package/apps/storefront/node_modules/.bin/prettier +43 -0
  22. package/apps/storefront/node_modules/.bin/rollup +43 -0
  23. package/apps/storefront/node_modules/.bin/tsc +43 -0
  24. package/apps/storefront/node_modules/.bin/tsserver +43 -0
  25. package/apps/storefront/node_modules/.bin/tsx +43 -0
  26. package/apps/storefront/node_modules/.bin/vite +43 -0
  27. package/apps/storefront/node_modules/.bin/vitest +43 -0
  28. package/apps/storefront/node_modules/.bin/workerd +43 -0
  29. package/apps/storefront/node_modules/.bin/wrangler +43 -0
  30. package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
  31. package/apps/storefront/node_modules/.bin/yaml +43 -0
  32. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
  33. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
  34. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
  35. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
  36. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
  37. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
  38. package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
  39. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
  40. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
  41. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  42. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  43. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
  44. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  45. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  46. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  47. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  48. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  49. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
  50. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
  51. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
  52. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
  53. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
  54. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
  55. package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
  56. package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
  57. package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
  58. package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
  59. package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
  60. package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
  61. package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
  62. package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
  63. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
  64. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
  65. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
  66. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
  67. package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
  68. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
  69. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
  70. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
  71. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
  72. package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
  73. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
  74. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
  75. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
  76. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
  77. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
  78. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
  79. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
  80. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
  81. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
  82. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
  83. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
  84. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
  85. package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
  86. package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
  87. package/apps/storefront/src/components/chrome/MegaMenu.astro +11 -1
  88. package/apps/storefront/src/components/chrome/NavDropdown.astro +49 -7
  89. package/apps/storefront/src/components/chrome/SiteFooter.astro +29 -4
  90. package/apps/storefront/src/components/chrome/SiteHeader.astro +39 -6
  91. package/apps/storefront/src/components/chrome/UtilityNav.astro +13 -1
  92. package/apps/storefront/src/components/islands/MobileNav.tsx +42 -2
  93. package/apps/storefront/src/layouts/Layout.astro +65 -7
  94. package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
  95. package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
  96. package/apps/storefront/src/lib/chrome/ctaIdentity.ts +21 -0
  97. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +22 -22
  98. package/apps/storefront/src/lib/chrome/trackingPlacements.ts +36 -0
  99. package/apps/storefront/src/lib/d1/schema-apps.ts +57 -0
  100. package/apps/storefront/src/lib/rawChrome.ts +73 -14
  101. package/apps/storefront/src/lib/rawMarketingHtml.ts +30 -0
  102. package/apps/storefront/src/lib/shared/capability.ts +68 -0
  103. package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
  104. package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
  105. package/apps/storefront/src/lib/storyblok/provider.ts +21 -5
  106. package/apps/storefront/src/lib/tracking/collectorAllowlist.ts +40 -0
  107. package/apps/storefront/src/lib/tracking/collectorIngest.ts +188 -0
  108. package/apps/storefront/src/lib/tracking/d1Control.ts +180 -0
  109. package/apps/storefront/src/lib/tracking/interactionCollector.ts +202 -0
  110. package/apps/storefront/src/lib/tracking/interactionHealth.ts +252 -0
  111. package/apps/storefront/src/lib/tracking/interactionSchema.ts +107 -0
  112. package/apps/storefront/src/lib/tracking/interactionStore.ts +0 -0
  113. package/apps/storefront/src/lib/tracking/pruneRollup.ts +260 -0
  114. package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +162 -0
  115. package/apps/storefront/src/lib/tracking/tenantD1Registry.ts +214 -0
  116. package/apps/storefront/src/lib/tracking/tenantReadAccess.ts +138 -0
  117. package/apps/storefront/src/lib/tracking-writeback/rewriteTrackingIds.ts +175 -0
  118. package/apps/storefront/src/lib/tracking-writeback/writebackCandidate.ts +110 -0
  119. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
  120. package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
  121. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
  122. package/apps/storefront/src/pages/api/admin/interaction-insights.ts +101 -0
  123. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
  124. package/apps/storefront/src/pages/api/tracking/interaction.ts +123 -0
  125. package/apps/storefront/src/pages/index.astro +1 -1
  126. package/apps/storefront/src/pages/products/[handle].astro +1 -1
  127. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +33 -14
  128. package/apps/storefront/src/workers/pruneRollupWorker.ts +120 -0
  129. package/package.json +1 -1
  130. package/packages/public-runtime/node_modules/.bin/tsc +43 -0
  131. package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
  132. package/packages/public-runtime/node_modules/.bin/vitest +43 -0
  133. package/packages/public-runtime/src/chrome.ts +18 -7
  134. package/packages/public-runtime/src/index.ts +10 -0
  135. package/packages/public-runtime/src/integration/index.ts +1 -0
  136. package/packages/public-runtime/src/integration/interaction-tracking-gate.ts +142 -0
  137. package/packages/public-runtime/src/interaction-tracking.ts +364 -0
  138. package/packages/public-runtime/src/tracking-id-derivation.ts +1008 -0
  139. package/pnpm-workspace.yaml +3 -0
  140. package/scripts/tenant/validate.mjs +306 -1
  141. package/scripts/tot-dev.mjs +59 -3
  142. package/tenants/home/public/js/storefront.js +6 -87
  143. package/apps/storefront/src/lib/apps/adminService.ts +0 -116
  144. package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
  145. package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
  146. package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
  147. package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
  148. package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
  149. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
  150. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
  151. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
  152. package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
  153. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
  154. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
  155. package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
  156. package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
  157. package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
  158. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
  159. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
  160. package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
  161. package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
  162. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
  163. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
  164. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
  165. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
  166. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
  167. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
  168. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
  169. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
  170. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
  171. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
  172. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
  173. package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
  174. package/apps/storefront/src/lib/content-edit/client.ts +0 -286
  175. package/apps/storefront/src/lib/content-edit/index.ts +0 -14
  176. package/apps/storefront/src/lib/content-edit/route.ts +0 -25
  177. package/apps/storefront/src/lib/content-edit/types.ts +0 -19
  178. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import type { ChromeLink, SocialLink } from "@/lib/chrome/model";
9
9
  import { withBase } from "@/lib/basePath";
10
+ import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
11
+ import { trackingAttributes } from "@tot/public-runtime";
10
12
 
11
13
  interface Props {
12
14
  links?: ChromeLink[];
@@ -19,6 +21,15 @@ interface Props {
19
21
  const { links = [], locale, currency, social = [], class: className } = Astro.props;
20
22
  const { basePath } = Astro.locals;
21
23
  const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
24
+ const linkStamp = (l: ChromeLink, index: number) =>
25
+ trackingAttributes({
26
+ el: l.id,
27
+ action: l.action,
28
+ tracking: l.tracking,
29
+ placement: CHROME_PLACEMENT.utilityNav,
30
+ scope: "utility-nav",
31
+ index,
32
+ });
22
33
  ---
23
34
 
24
35
  <div
@@ -30,12 +41,13 @@ const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.hr
30
41
  <div class="container-prose flex h-9 items-center justify-between gap-4 text-[0.72rem]">
31
42
  <ul class="flex items-center gap-5">
32
43
  {
33
- links.map((l) => (
44
+ links.map((l, i) => (
34
45
  <li>
35
46
  <a
36
47
  href={hrefOf(l)}
37
48
  class="transition-colors hover:text-primary"
38
49
  {...(l.external ? { rel: "noopener noreferrer" } : {})}
50
+ {...linkStamp(l, i)}
39
51
  >
40
52
  {l.label}
41
53
  </a>
@@ -12,14 +12,30 @@
12
12
  */
13
13
  import { useCallback, useEffect, useRef, useState } from "preact/hooks";
14
14
  import { isCurrentNavHref } from "@/lib/chrome/navCurrent";
15
+ import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
16
+ import { trackingAttributes } from "@tot/public-runtime";
15
17
 
18
+ /**
19
+ * `action`/`tracking` are declared loosely here (not the strict
20
+ * `TrackingDeclaration` union `SiteHeader.astro` maps from) — a fresh object
21
+ * literal spreading a discriminated union's fields widens to two independent
22
+ * optional properties, so TS can no longer see "exactly one of the two" at
23
+ * the type level. `trackingAttributes()` is still the runtime oracle: it
24
+ * throws on an invalid combination regardless of what the type allows.
25
+ */
16
26
  interface NavChild {
27
+ id: string;
17
28
  label: string;
18
29
  href: string;
30
+ action?: string;
31
+ tracking?: "none";
19
32
  }
20
33
  interface NavItem {
34
+ id: string;
21
35
  label: string;
22
36
  href: string;
37
+ action?: string;
38
+ tracking?: "none";
23
39
  children?: NavChild[];
24
40
  }
25
41
 
@@ -212,7 +228,7 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
212
228
 
213
229
  <nav aria-label="Mobile" class="flex-1 px-2 py-4">
214
230
  <ul class="space-y-1">
215
- {nav.map((item) => (
231
+ {nav.map((item, i) => (
216
232
  <li key={item.href}>
217
233
  {item.children && item.children.length > 0 ? (
218
234
  <div class="px-3 py-2">
@@ -222,11 +238,19 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
222
238
  currentPath && isCurrentNavHref(currentPath, item.href) ? "page" : undefined
223
239
  }
224
240
  class="block font-display text-base font-medium text-text hover:text-primary"
241
+ {...trackingAttributes({
242
+ el: item.id,
243
+ action: item.action,
244
+ tracking: item.tracking,
245
+ placement: CHROME_PLACEMENT.mobileMenu,
246
+ scope: "nav-mobile",
247
+ index: i,
248
+ })}
225
249
  >
226
250
  {item.label}
227
251
  </a>
228
252
  <ul class="mt-1 space-y-0.5 border-l border-border pl-3">
229
- {item.children.map((child) => (
253
+ {item.children.map((child, ci) => (
230
254
  <li key={child.href}>
231
255
  <a
232
256
  href={child.href}
@@ -234,6 +258,14 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
234
258
  currentPath && isCurrentNavHref(currentPath, child.href) ? "page" : undefined
235
259
  }
236
260
  class="block rounded px-2 py-1.5 font-body text-sm text-muted hover:bg-border/40 hover:text-text"
261
+ {...trackingAttributes({
262
+ el: child.id,
263
+ action: child.action,
264
+ tracking: child.tracking,
265
+ placement: CHROME_PLACEMENT.mobileMenu,
266
+ scope: `nav-mobile-mega:${item.id}`,
267
+ index: ci,
268
+ })}
237
269
  >
238
270
  {child.label}
239
271
  </a>
@@ -248,6 +280,14 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
248
280
  currentPath && isCurrentNavHref(currentPath, item.href) ? "page" : undefined
249
281
  }
250
282
  class="block rounded px-3 py-2.5 font-display text-base font-medium text-text hover:bg-border/40 hover:text-primary"
283
+ {...trackingAttributes({
284
+ el: item.id,
285
+ action: item.action,
286
+ tracking: item.tracking,
287
+ placement: CHROME_PLACEMENT.mobileMenu,
288
+ scope: "nav-mobile",
289
+ index: i,
290
+ })}
251
291
  >
252
292
  {item.label}
253
293
  </a>
@@ -23,12 +23,13 @@ import { referenceTheme } from "@/themes/reference";
23
23
  import { mergeTheme } from "@/themes/schema";
24
24
  import { organizationLd, websiteLd } from "@/lib/jsonld";
25
25
  import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
26
+ import { reportTrackingChainHealth } from "@/lib/tracking/interactionHealth";
26
27
  import { resolveHeaderVariant, resolveFooterVariant } from "@/lib/chrome/variant";
27
28
  import type { HeaderVariant, FooterVariant } from "@/lib/chrome/model";
28
- import { renderIntegrationPhases } from "@tot/public-runtime";
29
+ import { renderIntegrationPhases, INTERACTION_TRACKING_BOOT_JS } from "@tot/public-runtime";
29
30
  import { readViewerSession } from "@/lib/auth/route";
30
31
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
31
- import { getRegistryService } from "@/lib/apps/adminService.js";
32
+ import { getRegistryService } from "@/lib/apps/registryAccess.js";
32
33
  import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
33
34
 
34
35
  interface Props {
@@ -130,6 +131,15 @@ const resolvedFooterVariant = resolveFooterVariant(footerVariant, chrome.footer.
130
131
  // stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
131
132
  // chrome.html keep the token-driven Header/Footer (below).
132
133
  const chromeHtml = await Astro.locals.content.getChromeHtml(path);
134
+ if (chromeHtml) {
135
+ // HEALTH: this is the sharedChrome BYPASS path — a hand-authored chrome.html
136
+ // wins outright over the governed chrome components below, so nothing
137
+ // validated its tracking declarations and nothing stamped them. A wholly
138
+ // unstamped wrapper means no click on this page's chrome can ever be
139
+ // collected, and every other layer stays silent about it (absence is also how
140
+ // a legitimate opt-out looks). One structured line per surface, never a throw.
141
+ reportTrackingChainHealth(chromeHtml, { surface: `sharedChrome:${tenant.tenant_id}` });
142
+ }
133
143
  let sharedChrome: { header: string; footer: string; css: string[] } | null = null;
134
144
  if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
135
145
  const headEnd = chromeHtml.indexOf("</head>");
@@ -277,13 +287,44 @@ const footerWidgets = await renderWidgetSlot(
277
287
  alternates={alternates}
278
288
  />
279
289
 
290
+ {
291
+ /* Cloudflare Web Analytics is a non-essential measurement destination —
292
+ gate it on the SAME neutral consent broker every vendor loader reads
293
+ (ext-kernel-04 `consent.defaults` phase, published above as
294
+ `window.__TOT_INTEGRATION_CONSENT__`). The token rides a <meta> tag
295
+ (any script attribute, incl. define:vars, forces is:inline and drops
296
+ module processing — this script needs a real import) rather than a
297
+ page global; the script fail-closes like every other consumer of that
298
+ global: an absent/tampered snapshot denies, so an unconfigured CMP
299
+ means this never loads rather than loading ungated. */
300
+ analyticsEnabled && <meta name="cf-beacon-token" content={analyticsToken} />
301
+ }
280
302
  {
281
303
  analyticsEnabled && (
282
- <script
283
- defer
284
- src="https://static.cloudflareinsights.com/beacon.min.js"
285
- data-cf-beacon={`{"token": "${analyticsToken}"}`}
286
- />
304
+ <script>
305
+ import {
306
+ parseConsentSnapshot,
307
+ isPurposeEligible,
308
+ defaultConsentSnapshot,
309
+ INTEGRATION_CONSENT_GLOBAL,
310
+ } from "@tot/public-runtime";
311
+
312
+ const snapshot =
313
+ parseConsentSnapshot((window as unknown as Record<string, unknown>)[INTEGRATION_CONSENT_GLOBAL]) ??
314
+ defaultConsentSnapshot();
315
+
316
+ const token = document
317
+ .querySelector('meta[name="cf-beacon-token"]')
318
+ ?.getAttribute("content");
319
+
320
+ if (token && isPurposeEligible(snapshot, "analytics")) {
321
+ const beacon = document.createElement("script");
322
+ beacon.defer = true;
323
+ beacon.src = "https://static.cloudflareinsights.com/beacon.min.js";
324
+ beacon.setAttribute("data-cf-beacon", JSON.stringify({ token }));
325
+ document.head.appendChild(beacon);
326
+ }
327
+ </script>
287
328
  )
288
329
  }
289
330
  </head>
@@ -554,6 +595,23 @@ const footerWidgets = await renderWidgetSlot(
554
595
  bootTamperSentinel();
555
596
  </script>
556
597
 
598
+ {/* Declarative interaction tracking (dt-contract-v1) — ONE delegated click
599
+ listener, installed once per document, that resolves the nearest
600
+ ancestor carrying `data-tot-el` and reports its declared
601
+ identity/action/placement (plus the data-tot-scope/data-tot-i instance
602
+ pair). It reads declared attributes ONLY: no textContent/innerText/alt/
603
+ value/placeholder, no form-field reads. Delivery, consent gating and
604
+ environment gating are NOT here — each interaction is dispatched as a
605
+ `tot:interaction` CustomEvent for the collector to consume.
606
+
607
+ This is the Astro-mount half of the install; `rawHtmlResponse()` is the
608
+ other (ADR 0015 one-engine-two-mounts). Both embed the SAME serialized
609
+ listener, and the install-once marker makes a double install a no-op —
610
+ which is what makes the raw chrome wrapper spliced into this document
611
+ (above) safe. Emitted as `is:inline` + the page nonce so it needs no
612
+ entry in lib/cspStaticHashes.ts. */}
613
+ <script is:inline nonce={cspNonce} set:html={INTERACTION_TRACKING_BOOT_JS}></script>
614
+
557
615
  {/* document.deferred global-entry boundary (ext-kernel-04) — the last phase,
558
616
  the renderer-owned mount point for the deferred integration entry. */}
559
617
  <Fragment set:html={integrationPhases.bodyDeferred} />
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Renderer-shared registry access — split out of `adminService.ts` (2026-09-02
3
+ * audit) because `getRegistryService`/`json` are needed by the actual page
4
+ * render path (Layout.astro, index.astro, products/[handle].astro resolve
5
+ * installed app widgets on every request) AND by the webhook dispatcher
6
+ * (`getDispatcher.ts`/`emit.ts`), while the REST of `adminService.ts`
7
+ * (`getManagedAnalyticsService`, `registryErrorResponse`, `parseInstallId`,
8
+ * `requireOwnedInstall`, `requireManageRole`) is reached only from the
9
+ * operator-only `/api/apps/admin/*` cluster. ESM loads a whole file's imports
10
+ * regardless of which export is used, so keeping both halves in one file meant
11
+ * every page render transitively pulled in the admin-only chain too (and made
12
+ * `adminService.ts` impossible to drop from the public storefront-runner
13
+ * without also breaking the renderer). This file is the renderer-safe half;
14
+ * `adminService.ts` re-exports `json` from here for its own admin-only helpers.
15
+ */
16
+ import { AppRegistryService } from "./registryService.js";
17
+ import { D1CredentialStore } from "./credentials.js";
18
+ import { DEFAULT_APP_GATEWAY_KID, APP_GATEWAY_KID_ENV } from "./gatewayKeys.js";
19
+ import { fromD1 } from "../d1/catalog.js";
20
+ import { readD1, readEnv } from "../env.js";
21
+
22
+ /**
23
+ * No-store JSON response — install/rotate bodies carry a one-time client
24
+ * secret and must never be edge- or browser-cached.
25
+ */
26
+ export function json(body: unknown, status = 200): Response {
27
+ return new Response(JSON.stringify(body), {
28
+ status,
29
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Build an `AppRegistryService` over the real `STOREFRONT_APPS_DB` D1
35
+ * binding, or return a 503 Response when that binding isn't configured in
36
+ * this runtime. Never leaks binding/secret detail in the response body.
37
+ */
38
+ export async function getRegistryService(): Promise<AppRegistryService | Response> {
39
+ const d1 = await readD1("STOREFRONT_APPS_DB");
40
+ if (!d1) return json({ error: "app registry not configured" }, 503);
41
+ const db = fromD1(d1);
42
+ const activeKid = (await readEnv(APP_GATEWAY_KID_ENV)) ?? DEFAULT_APP_GATEWAY_KID;
43
+ const credentials = new D1CredentialStore(db, activeKid);
44
+ return new AppRegistryService(db, credentials);
45
+ }
@@ -30,13 +30,13 @@
30
30
  */
31
31
  import type { SessionRecord } from "./session.js";
32
32
  import { sessionHostCapability } from "./loginGate.js";
33
- import { decideIsOwner } from "@/lib/change/route/route";
34
33
  import {
34
+ decideIsOwner,
35
35
  resolveShipCapabilities,
36
+ OWNER_CAPABILITIES,
37
+ type Capability,
36
38
  type ShipCapabilities,
37
- } from "@/lib/change/route/ship-capabilities";
38
- import type { Capability } from "@/lib/change/types";
39
- import { OWNER_CAPABILITIES } from "@/lib/grants/session";
39
+ } from "@/lib/shared/capability";
40
40
 
41
41
  /** Which entry case a viewer is in, for chrome + the DoD test matrix. */
42
42
  export type AdminEntryPrincipal =
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Governed-CTA identity attribute — the ONE mapping from a `ChromeCta` config
3
+ * to the markup identity every renderer stamps onto its rendered anchor.
4
+ *
5
+ * Two render paths draw on the same governed `ChromeCta` config: `rawChrome.ts`
6
+ * (the RAW-page chrome wrapper) and `SiteHeader.astro` (the app-Layout header).
7
+ * Both must emit the identical identity attribute for the identical CTA, so a
8
+ * downstream consumer (tracking, an operator's own selectors) never has to care
9
+ * which surface rendered the page. Keeping the derivation in ONE function,
10
+ * imported by both, makes that a structural guarantee rather than a convention
11
+ * two files have to remember to keep in sync.
12
+ */
13
+ import type { ChromeCta } from "./model.js";
14
+
15
+ /** The identity attribute name governed CTAs are stamped with. */
16
+ export const CTA_IDENTITY_ATTR = "data-cta-id";
17
+
18
+ /** The (name, value) pair a governed CTA's config resolves to for markup identity. */
19
+ export function ctaIdentityAttr(cta: ChromeCta): { name: string; value: string } {
20
+ return { name: CTA_IDENTITY_ATTR, value: cta.id };
21
+ }
@@ -18,15 +18,15 @@
18
18
  }
19
19
  },
20
20
  "nav": [
21
- { "id": "nav-platform", "label": "Platform", "href": "/#platform" },
22
- { "id": "nav-industries", "label": "Industries", "href": "/#industries" },
23
- { "id": "nav-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
24
- { "id": "nav-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
25
- { "id": "nav-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
21
+ { "id": "nav-platform", "action": "nav-platform", "label": "Platform", "href": "/#platform" },
22
+ { "id": "nav-industries", "action": "nav-industries", "label": "Industries", "href": "/#industries" },
23
+ { "id": "nav-resources", "action": "nav-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
24
+ { "id": "nav-pricing", "action": "nav-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
25
+ { "id": "nav-login", "action": "nav-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
26
26
  ],
27
27
  "ctas": [
28
- { "id": "cta-book-demo", "label": "Book Demo", "href": "https://tokenoftrust.com/contact/", "variant": "secondary" },
29
- { "id": "cta-start-free", "label": "Start For Free", "href": "https://app.tokenoftrust.com", "variant": "primary" }
28
+ { "id": "cta-book-demo", "action": "cta-book-demo", "label": "Book Demo", "href": "https://tokenoftrust.com/contact/", "variant": "secondary" },
29
+ { "id": "cta-start-free", "action": "cta-start-for-free", "label": "Start For Free", "href": "https://app.tokenoftrust.com", "variant": "primary" }
30
30
  ]
31
31
  },
32
32
  "footer": {
@@ -38,36 +38,36 @@
38
38
  {
39
39
  "title": "Platform",
40
40
  "links": [
41
- { "id": "ftr-platform-idv", "label": "Identity Verification", "href": "/#platform" },
42
- { "id": "ftr-platform-age", "label": "Age Assurance", "href": "/#platform" },
43
- { "id": "ftr-platform-fraud", "label": "Fraud Prevention", "href": "/#platform" },
44
- { "id": "ftr-platform-evidence", "label": "Compliance Evidence", "href": "/#platform" },
45
- { "id": "ftr-platform-aml", "label": "AML · PACT Act · Tax", "href": "/#platform" }
41
+ { "id": "ftr-platform-idv", "action": "nav-identity-verification", "label": "Identity Verification", "href": "/#platform" },
42
+ { "id": "ftr-platform-age", "action": "nav-age-assurance", "label": "Age Assurance", "href": "/#platform" },
43
+ { "id": "ftr-platform-fraud", "action": "nav-fraud-prevention", "label": "Fraud Prevention", "href": "/#platform" },
44
+ { "id": "ftr-platform-evidence", "action": "nav-compliance-evidence", "label": "Compliance Evidence", "href": "/#platform" },
45
+ { "id": "ftr-platform-aml", "action": "nav-aml-pact-act-tax", "label": "AML · PACT Act · Tax", "href": "/#platform" }
46
46
  ]
47
47
  },
48
48
  {
49
49
  "title": "Industries",
50
50
  "links": [
51
- { "id": "ftr-industries-alcohol", "label": "Alcohol", "href": "/#industries" },
52
- { "id": "ftr-industries-vape", "label": "Tobacco & Vape", "href": "/#industries" },
53
- { "id": "ftr-industries-age-restricted", "label": "Age-Restricted Goods", "href": "/#industries" },
54
- { "id": "ftr-industries-marketplaces", "label": "Marketplaces", "href": "/#industries" }
51
+ { "id": "ftr-industries-alcohol", "action": "nav-alcohol", "label": "Alcohol", "href": "/#industries" },
52
+ { "id": "ftr-industries-vape", "action": "nav-tobacco-and-vape", "label": "Tobacco & Vape", "href": "/#industries" },
53
+ { "id": "ftr-industries-age-restricted", "action": "nav-age-restricted-goods", "label": "Age-Restricted Goods", "href": "/#industries" },
54
+ { "id": "ftr-industries-marketplaces", "action": "nav-marketplaces", "label": "Marketplaces", "href": "/#industries" }
55
55
  ]
56
56
  },
57
57
  {
58
58
  "title": "Company",
59
59
  "links": [
60
- { "id": "ftr-company-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
61
- { "id": "ftr-company-contact", "label": "Contact Sales", "href": "https://tokenoftrust.com/contact/", "external": true },
62
- { "id": "ftr-company-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
63
- { "id": "ftr-company-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
60
+ { "id": "ftr-company-pricing", "action": "nav-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
61
+ { "id": "ftr-company-contact", "action": "nav-contact-sales", "label": "Contact Sales", "href": "https://tokenoftrust.com/contact/", "external": true },
62
+ { "id": "ftr-company-resources", "action": "nav-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
63
+ { "id": "ftr-company-login", "action": "nav-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
64
64
  ]
65
65
  },
66
66
  {
67
67
  "title": "Legal",
68
68
  "links": [
69
- { "id": "ftr-legal-privacy", "label": "Privacy Policy", "href": "https://tokenoftrust.com/privacy/", "external": true },
70
- { "id": "ftr-legal-terms", "label": "Terms", "href": "https://tokenoftrust.com/terms/", "external": true }
69
+ { "id": "ftr-legal-privacy", "action": "nav-privacy-policy", "label": "Privacy Policy", "href": "https://tokenoftrust.com/privacy/", "external": true },
70
+ { "id": "ftr-legal-terms", "action": "nav-terms", "label": "Terms", "href": "https://tokenoftrust.com/terms/", "external": true }
71
71
  ]
72
72
  }
73
73
  ],
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Canonical `data-tot-placement` values for governed chrome + navigation (V1
3
+ * pilot scope: chrome CTAs, primary nav, mega-menu children, utility nav,
4
+ * footer, mobile nav — see docs/declarative-interaction-tracking-contract.md
5
+ * and decision v1-scope-chrome-nav-pilot). ONE source of truth shared by both
6
+ * the component renderer (SiteHeader.astro / MegaMenu.astro / NavDropdown.astro
7
+ * / MobileNav.tsx / UtilityNav.astro / SiteFooter.astro) and the raw-string
8
+ * renderer (rawChrome.ts), so the same section can never emit a different
9
+ * placement literal on the two surfaces.
10
+ *
11
+ * Placement is always passed explicitly by whichever renderer knows where it
12
+ * is rendering (decision identity-action-placement-split-v2) — never inferred
13
+ * from DOM position, component nesting, or render mode.
14
+ */
15
+ export const CHROME_PLACEMENT = {
16
+ /** Header-actions cluster CTAs (rawChrome's `.header-cta`, SiteHeader's `headerCtas`). */
17
+ header: "header",
18
+ /** Primary-nav top-level items — desktop bar (raw `.primary-nav`, MegaMenu/NavDropdown mode="bar"). */
19
+ primaryNav: "primary-nav",
20
+ /** Mega-menu / single-column dropdown children, on either surface. */
21
+ megaMenu: "mega-menu",
22
+ /** The thin utility bar above the header. */
23
+ utilityNav: "utility-nav",
24
+ /** Footer link columns. */
25
+ footer: "footer",
26
+ /**
27
+ * Footer bottom legal/policy link row — component surface only. The raw
28
+ * renderer's `ChromeFooter.bottom` carries text only (no links); its footer
29
+ * columns cover the same links under `footer` above.
30
+ */
31
+ footerBottom: "footer-bottom",
32
+ /** The mobile nav drawer (MobileNav island) and its no-JS accordion fallback. */
33
+ mobileMenu: "mobile-menu",
34
+ } as const;
35
+
36
+ export type ChromePlacement = (typeof CHROME_PLACEMENT)[keyof typeof CHROME_PLACEMENT];
@@ -149,6 +149,63 @@ export const activityEvents = sqliteTable(
149
149
  }),
150
150
  );
151
151
 
152
+ // --- Per-tenant raw-interaction store directory ------------------------------
153
+ // The DIRECTORY of per-tenant D1 databases that hold raw declarative-tracking
154
+ // interaction rows — one database per tenant (`lib/tracking/interactionSchema.ts`
155
+ // is that database's schema). This table holds NO interaction data: only the
156
+ // `tenant_id → (database_name, database_uuid)` mapping the Worker needs to
157
+ // address a tenant's own database over the D1 REST API, since Worker bindings
158
+ // are static and cannot be created per tenant at runtime.
159
+ //
160
+ // It lives in the SHARED STOREFRONT_APPS_DB on purpose: a directory of tenants
161
+ // is inherently cross-tenant, and keeping it here means the isolation boundary
162
+ // stays exactly one hop — a lookup returns ONE tenant's database handle, and
163
+ // that database contains nothing else. `tenant_id` is the primary key, so one
164
+ // tenant can never map to two databases (and two tenants can never share one).
165
+ //
166
+ // This is the seam the follow-on units read:
167
+ // • `dt-tenant-isolation` — resolves the caller's authorized tenant to
168
+ // exactly one `database_uuid`; a merchant read
169
+ // that is not in their own row cannot be built.
170
+ // • `dt-prune-and-rollup` — ENUMERATES this table to sweep every tenant's
171
+ // 90-day window (`last_pruned_at` / `pruned_rows`
172
+ // are its bookkeeping), and uses `size_bytes` /
173
+ // `size_checked_at` to surface a tenant nearing
174
+ // D1's 10GB per-database ceiling.
175
+ export const trackingTenantDatabases = sqliteTable(
176
+ "tracking_tenant_databases",
177
+ {
178
+ tenantId: text("tenant_id").primaryKey(),
179
+ // The Cloudflare D1 database name, derived deterministically from the tenant
180
+ // id (`lib/tracking/tenantD1Registry.ts`) — recorded so an operator can find
181
+ // it in the dashboard without re-deriving.
182
+ databaseName: text("database_name").notNull(),
183
+ // The D1 UUID — what the REST data plane actually addresses.
184
+ databaseUuid: text("database_uuid").notNull(),
185
+ // Applied `INTERACTION_SCHEMA_VERSION`; a bump re-applies the DDL per tenant.
186
+ schemaVersion: integer("schema_version").notNull(),
187
+ createdAt: text("created_at").notNull(),
188
+ updatedAt: text("updated_at").notNull(),
189
+ // Bookkeeping for the scheduled prune (written by `dt-prune-and-rollup`).
190
+ lastPrunedAt: text("last_pruned_at"),
191
+ prunedRows: integer("pruned_rows").notNull().default(0),
192
+ // Last observed database size + when, for the 10GB-ceiling warning.
193
+ sizeBytes: integer("size_bytes"),
194
+ sizeCheckedAt: text("size_checked_at"),
195
+ },
196
+ (t) => ({
197
+ // Two tenants sharing one database would defeat the whole structural-
198
+ // isolation claim, so make it impossible at the schema level.
199
+ uqTrackingTenantDatabasesUuid: uniqueIndex("uq_tracking_tenant_databases_uuid").on(
200
+ t.databaseUuid,
201
+ ),
202
+ // The prune sweep's work queue: oldest-pruned first.
203
+ idxTrackingTenantDatabasesPruned: index("idx_tracking_tenant_databases_pruned").on(
204
+ t.lastPrunedAt,
205
+ ),
206
+ }),
207
+ );
208
+
152
209
  // --- Webhook event spine ----------------------------------------------------
153
210
  // Immutable event log. `id` is the semantic event id we mint — distinct from
154
211
  // any per-delivery wire envelope id (see `webhookDeliveries.deliveryId` below).