@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
@@ -26,6 +26,9 @@
26
26
  */
27
27
  import { CHROME_BODY_MARKER, CHROME_HEAD_MARKER } from "./rawMarketingHtml.js";
28
28
  import { isCurrentNavHref } from "./chrome/navCurrent.js";
29
+ import { ctaIdentityAttr } from "./chrome/ctaIdentity.js";
30
+ import { CHROME_PLACEMENT } from "./chrome/trackingPlacements.js";
31
+ import { trackingAttributesHtml } from "@tot/public-runtime";
29
32
  import type {
30
33
  ChromeAgeGate,
31
34
  ChromeAnnouncement,
@@ -165,12 +168,27 @@ function renderAnnouncement(a: ChromeAnnouncement): string {
165
168
 
166
169
  function renderUtilityNav(u: ChromeUtilityNav): string {
167
170
  const links = (u.links ?? [])
168
- .map((l) => `<li><a href="${esc(l.href)}"${relFor(l)}>${esc(l.label)}</a></li>`)
171
+ .map((l, i) => {
172
+ const stamp = trackingAttributesHtml({
173
+ el: l.id,
174
+ action: l.action,
175
+ tracking: l.tracking,
176
+ placement: CHROME_PLACEMENT.utilityNav,
177
+ scope: "utility-nav",
178
+ index: i,
179
+ });
180
+ return `<li><a href="${esc(l.href)}"${relFor(l)}${stamp}>${esc(l.label)}</a></li>`;
181
+ })
169
182
  .join("");
170
183
  const locale = (u.locale ?? []).map((s) => `<span class="sel">${esc(s)}</span>`).join("");
171
184
  const social = u.social?.length ? `<span class="social">${renderSocial(u.social)}</span>` : "";
172
185
  const cue = u.memberCue
173
- ? `<a class="member-cue" href="${esc(u.memberCue.href)}">${esc(u.memberCue.label)}</a>`
186
+ ? `<a class="member-cue" href="${esc(u.memberCue.href)}"${trackingAttributesHtml({
187
+ el: u.memberCue.id,
188
+ action: u.memberCue.action,
189
+ tracking: u.memberCue.tracking,
190
+ placement: CHROME_PLACEMENT.utilityNav,
191
+ })}>${esc(u.memberCue.label)}</a>`
174
192
  : "";
175
193
  const right = `${locale}${social}${cue}`;
176
194
  return (
@@ -194,7 +212,7 @@ function renderUtilityNav(u: ChromeUtilityNav): string {
194
212
  * unprefixed and exact-match-only (see navCurrent.ts). Omitted ⇒ no item is
195
213
  * ever marked current (callers that don't know the request path, e.g. tests).
196
214
  */
197
- function renderNavItem(item: ChromeNavItem, currentPath?: string): string {
215
+ function renderNavItem(item: ChromeNavItem, index: number, currentPath?: string): string {
198
216
  const columns = item.columns?.length
199
217
  ? item.columns
200
218
  : item.children?.length
@@ -207,9 +225,19 @@ function renderNavItem(item: ChromeNavItem, currentPath?: string): string {
207
225
  const haspopup = hasPanel ? ' aria-haspopup="true"' : "";
208
226
  const itemCurrent = currentPath !== undefined && isCurrentNavHref(currentPath, item.href);
209
227
  const ariaCurrent = itemCurrent ? ' aria-current="page"' : "";
210
- const link = `<a${linkClass} href="${esc(item.href)}"${haspopup}${ariaCurrent}>${esc(item.label)}${badge}${caret}</a>`;
228
+ const itemStamp = trackingAttributesHtml({
229
+ el: item.id,
230
+ action: item.action,
231
+ tracking: item.tracking,
232
+ placement: CHROME_PLACEMENT.primaryNav,
233
+ scope: "nav-primary",
234
+ index,
235
+ });
236
+ const link = `<a${linkClass} href="${esc(item.href)}"${haspopup}${ariaCurrent}${itemStamp}>${esc(item.label)}${badge}${caret}</a>`;
211
237
  if (!hasPanel) return `<li class="nav-item">${link}</li>`;
212
238
 
239
+ const megaScope = `nav-mega:${item.id}`;
240
+ let childIndex = 0;
213
241
  const wide = columns.length > 1 ? " mega--wide" : "";
214
242
  const cols = columns
215
243
  .map((c) => {
@@ -219,7 +247,15 @@ function renderNavItem(item: ChromeNavItem, currentPath?: string): string {
219
247
  const childCurrent =
220
248
  currentPath !== undefined && isCurrentNavHref(currentPath, l.href, l.external);
221
249
  const childAriaCurrent = childCurrent ? ' aria-current="page"' : "";
222
- return `<a href="${esc(l.href)}"${relFor(l)}${childAriaCurrent}>${esc(l.label)}</a>`;
250
+ const childStamp = trackingAttributesHtml({
251
+ el: l.id,
252
+ action: l.action,
253
+ tracking: l.tracking,
254
+ placement: CHROME_PLACEMENT.megaMenu,
255
+ scope: megaScope,
256
+ index: childIndex++,
257
+ });
258
+ return `<a href="${esc(l.href)}"${relFor(l)}${childAriaCurrent}${childStamp}>${esc(l.label)}</a>`;
223
259
  })
224
260
  .join("");
225
261
  return `<div class="mega__col">${heading}${links}</div>`;
@@ -257,22 +293,35 @@ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?
257
293
  const minimal = variant === "minimal";
258
294
  // Header CTAs, on the same `!minimal` rule the Astro SiteHeader applies, so the
259
295
  // two surfaces render the same governed config identically. Each carries its
260
- // stable `id` as `data-cta-id`, so the governed identity is present in the
261
- // served DOM rather than implied by label or href.
296
+ // identity attribute (ctaIdentityAttr, shared with SiteHeader.astro), so the
297
+ // governed identity is present in the served DOM rather than implied by label
298
+ // or href.
262
299
  const ctas =
263
300
  minimal || !header.ctas?.length
264
301
  ? ""
265
302
  : header.ctas
266
- .map(
267
- (c) =>
303
+ .map((c, i) => {
304
+ const idAttr = ctaIdentityAttr(c);
305
+ const stamp = trackingAttributesHtml({
306
+ el: c.id,
307
+ action: c.action,
308
+ tracking: c.tracking,
309
+ placement: CHROME_PLACEMENT.header,
310
+ scope: "header-ctas",
311
+ index: i,
312
+ });
313
+ return (
268
314
  `<a class="header-cta header-cta--${esc(c.variant ?? "primary")}" ` +
269
- `data-cta-id="${esc(c.id)}" href="${esc(c.href)}">${esc(c.label)}</a>`,
270
- )
315
+ `${idAttr.name}="${esc(idAttr.value)}" href="${esc(c.href)}"${stamp}>${esc(c.label)}</a>`
316
+ );
317
+ })
271
318
  .join("");
272
319
  const hamburger = minimal
273
320
  ? ""
274
321
  : `<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primaryNav"><span></span><span></span><span></span></button>`;
275
- const navItems = header.nav.map((item) => renderNavItem(item, currentPath)).join("\n ");
322
+ const navItems = header.nav
323
+ .map((item, i) => renderNavItem(item, i, currentPath))
324
+ .join("\n ");
276
325
  const navBar = minimal
277
326
  ? ""
278
327
  : ` <nav class="primary-nav-bar" aria-label="Primary">\n` +
@@ -343,9 +392,19 @@ function renderFooter(footer: ChromeFooter): string {
343
392
  })()
344
393
  : "";
345
394
  const columns = footer.columns
346
- .map((c) => {
395
+ .map((c, colIndex) => {
347
396
  const links = c.links
348
- .map((l) => `<li><a href="${esc(l.href)}"${relFor(l)}>${esc(l.label)}</a></li>`)
397
+ .map((l, i) => {
398
+ const stamp = trackingAttributesHtml({
399
+ el: l.id,
400
+ action: l.action,
401
+ tracking: l.tracking,
402
+ placement: CHROME_PLACEMENT.footer,
403
+ scope: `footer-col:${colIndex}`,
404
+ index: i,
405
+ });
406
+ return `<li><a href="${esc(l.href)}"${relFor(l)}${stamp}>${esc(l.label)}</a></li>`;
407
+ })
349
408
  .join("");
350
409
  return ` <div class="footer-col">\n <h4>${esc(c.title)}</h4>\n <ul>${links}</ul>\n </div>`;
351
410
  })
@@ -17,8 +17,10 @@
17
17
  */
18
18
  import {
19
19
  renderIntegrationPhases,
20
+ INTERACTION_TRACKING_BOOT_JS,
20
21
  type EffectivePlan,
21
22
  } from "@tot/public-runtime";
23
+ import { reportTrackingChainHealth } from "./tracking/interactionHealth.js";
22
24
 
23
25
  export { rawHtmlPolicy } from "@tot/public-runtime";
24
26
 
@@ -113,6 +115,23 @@ function injectBodyEnd(html: string, snippet: string): string {
113
115
  return html + snippet;
114
116
  }
115
117
 
118
+ /**
119
+ * The declarative-interaction-tracking listener as a nonce-carrying inline
120
+ * `<script>` — the raw-mount half of the install (`Layout.astro` is the other,
121
+ * ADR 0015 one-engine-two-mounts). Both mounts embed the SAME serialized
122
+ * listener from `@tot/public-runtime`, so no markup path can drift into a
123
+ * second implementation, and the listener's install-once marker makes a second
124
+ * copy in the same document (a tenant `chrome.html` wrapper that also carries
125
+ * it) a no-op.
126
+ *
127
+ * Carries the nonce itself because it is spliced AFTER `nonceInlineTags` has
128
+ * run — same discipline as the render engine's phase markup.
129
+ */
130
+ export function interactionTrackingScript(nonce: string): string {
131
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
132
+ return `<script${nonceAttr}>${INTERACTION_TRACKING_BOOT_JS}</script>`;
133
+ }
134
+
116
135
  /** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
117
136
  export function nonceInlineTags(html: string, nonce: string): string {
118
137
  return html
@@ -167,6 +186,12 @@ export function rawHtmlResponse(
167
186
  isFullDocument(html) || !opts.chromeWrapper
168
187
  ? html
169
188
  : wrapFragmentInChrome(html, opts.chromeWrapper);
189
+ // HEALTH: a raw surface bypasses governed chrome, so `validateChromeConfig`
190
+ // never saw it and `trackingAttributes()` never stamped it. If NOT ONE of its
191
+ // actionable elements carries an identity, no click on this page can ever be
192
+ // collected — and every other layer is silent about that by design (absence
193
+ // is also how a legitimate opt-out looks). One structured line, never a throw.
194
+ reportTrackingChainHealth(composed, { surface: `raw:${opts.basePath || "/"}` });
170
195
  // DEV ONLY: these pages are served verbatim, bypassing Astro's render pipeline,
171
196
  // so the Vite HMR client is never injected — a save would reach the server (see
172
197
  // the tenantHotReload plugin in astro.config.mjs) but the browser would have no
@@ -201,6 +226,11 @@ export function rawHtmlResponse(
201
226
  composed = injectHeadStart(composed, phases.head);
202
227
  composed = injectBodyStart(composed, phases.bodyStart);
203
228
  composed = injectBodyEnd(composed, phases.bodyDeferred);
229
+ // Declarative interaction tracking, last in the body so the whole document —
230
+ // a verbatim tenant document, a fragment spliced into a hand-authored
231
+ // `chrome.html`, or a fragment spliced into the generated `renderRawChrome`
232
+ // wrapper — is already present when the delegated listener binds.
233
+ composed = injectBodyEnd(composed, interactionTrackingScript(opts.nonce));
204
234
  const doc = prefixTenantLinks(composed, opts.basePath);
205
235
  return new Response(doc, {
206
236
  headers: { "content-type": "text/html; charset=utf-8" },
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Ship/owner capability primitives — genuinely shared between the control-plane
3
+ * ship surface (`@/lib/change`, `@/lib/grants`, `@/lib/forge` — all moat, dropped
4
+ * from the public storefront-runner) and renderer-adjacent code that ships in the
5
+ * runner (`@/lib/auth/adminEntry`, the order-admin API guard). Pure types + pure
6
+ * functions only — no I/O, no MCP/forge calls, no secrets — which is exactly why
7
+ * they can live outside the moat cluster instead of forcing a KEPT file to import
8
+ * a DROPped one.
9
+ *
10
+ * Canonical home for these five exports; `@/lib/forge/types`, `@/lib/grants/session`,
11
+ * `@/lib/change/route/route`, and `@/lib/change/route/ship-capabilities` re-export
12
+ * from here (not the other way around) so the moat cluster still has one place to
13
+ * read them, while nothing outside the moat needs to import INTO it.
14
+ */
15
+
16
+ /** Access level the acting operator holds. */
17
+ export type Capability = "preview-access" | "ship-on-behalf";
18
+
19
+ /** Capabilities that carry tenant-owner standing (vs. a granted developer). */
20
+ export const OWNER_CAPABILITIES: ReadonlySet<string> = new Set(["owner", "admin"]);
21
+
22
+ /** Pure inputs to the owner-principal decision (session only, no I/O). */
23
+ export interface OwnerPrincipalInput {
24
+ /** Whether the request is authenticated for the resolved tenant. */
25
+ authorized: boolean;
26
+ /** The signed-in viewer's capability over the resolved tenant, if any. */
27
+ viewerCapability: string | undefined;
28
+ }
29
+
30
+ /**
31
+ * Decide whether the acting principal IS the tenant owner. Owner iff the
32
+ * request is authorized AND a signed-in viewer holds an owner/admin capability
33
+ * over the resolved tenant.
34
+ */
35
+ export function decideIsOwner(input: OwnerPrincipalInput): boolean {
36
+ if (!input.authorized) return false;
37
+ return !!input.viewerCapability && OWNER_CAPABILITIES.has(input.viewerCapability);
38
+ }
39
+
40
+ /**
41
+ * The acting principal's ship-relevant relationship to the tenant, resolved
42
+ * SERVER-SIDE (never from request input).
43
+ */
44
+ export interface ShipPrincipal {
45
+ /** True when the acting principal IS the tenant owner (see {@link decideIsOwner}). */
46
+ isOwner: boolean;
47
+ /** The developer grant capability the principal holds. Fail-closed default is `preview-access`. */
48
+ capability: Capability;
49
+ }
50
+
51
+ /** The two distinct ship-gate capabilities, derived per-principal. */
52
+ export interface ShipCapabilities {
53
+ /** May invoke ship-on-behalf (accept / reject / rollback). Owner is a superset. */
54
+ shipOnBehalf: boolean;
55
+ /** May invoke the apex (DNS) cutover. OWNER-ONLY. */
56
+ apexCutover: boolean;
57
+ }
58
+
59
+ /**
60
+ * Resolve the two capabilities from the principal. Owner ⊇ ship-on-behalf for the
61
+ * ship axis; apex-cutover is owner-only.
62
+ */
63
+ export function resolveShipCapabilities(principal: ShipPrincipal): ShipCapabilities {
64
+ return {
65
+ shipOnBehalf: principal.isOwner || principal.capability === "ship-on-behalf",
66
+ apexCutover: principal.isOwner,
67
+ };
68
+ }