@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
@@ -0,0 +1,123 @@
1
+ /**
2
+ * POST /api/tracking/interaction (dt-collector-v1) — the beacon sink for
3
+ * declarative interaction tracking.
4
+ *
5
+ * The browser collector (`@/lib/tracking/interactionCollector`) batches
6
+ * `TotInteraction`s captured off `installInteractionTracking` /
7
+ * `onInteraction` (`@tot/public-runtime`) — via `dt-consent-environment-gate`'s
8
+ * consent/environment wrapper in front of it — and POSTs them here with
9
+ * `navigator.sendBeacon`, same transport contract as the F14 RUM beacon
10
+ * (`pages/api/rum/vitals.ts`): unauthenticated (a beacon carries no
11
+ * credentials), tenant resolved from `context.locals.tenant.tenant_id`
12
+ * (never the body), 204 on every outcome so a beacon never errors, retries,
13
+ * or blocks page unload.
14
+ *
15
+ * VALIDATION — `collectorIngest.ts` re-derives every field from the raw body
16
+ * under a default-deny allowlist (`collectorAllowlist.ts`): an unlisted key is
17
+ * dropped and counted, a listed-but-wrong-typed field rejects that event. Never
18
+ * trust the client for `tenant_id` or `stream` — both are stamped here, always.
19
+ *
20
+ * STORAGE — two sinks, fanned out past the response (`interactionStore.ts`,
21
+ * same shape as `pages/api/activity.ts`'s `fanOutActivity`):
22
+ *
23
+ * • RAW ROWS → the tenant's OWN D1 database, resolved (and provisioned on
24
+ * first sight) by `tenantD1Registry.resolveTenantStore`. ONE DATABASE PER
25
+ * TENANT, so cross-tenant leakage is impossible by construction, not by a
26
+ * correct `WHERE tenant_id = ?`. The directory that maps tenant → database
27
+ * lives in the shared `STOREFRONT_APPS_DB`
28
+ * (`tracking_tenant_databases`); the rows never do.
29
+ * • AGGREGATES → the DEDICATED `INTERACTION_ANALYTICS` Analytics Engine
30
+ * dataset (`storefront_interactions`) — NOT the operational spine's
31
+ * `storefront_activity` dataset — rolled up to one point per dimension
32
+ * tuple, so no per-event identity reaches AE.
33
+ *
34
+ * Both sinks are best-effort: unconfigured or failing, the batch falls back to
35
+ * the Workers Logs firehose and the beacon still 204s. `stream` (always
36
+ * `"interaction"` today) is the discriminator column to route on if a second
37
+ * stream is ever added.
38
+ */
39
+ import type { APIContext } from "astro";
40
+ import { readD1, readAnalytics, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
41
+ import { fromD1 } from "@/lib/d1/catalog";
42
+ import { ingestInteractions, type StampedInteractionEvent } from "@/lib/tracking/collectorIngest";
43
+ import { readD1Control } from "@/lib/tracking/d1Control";
44
+ import { resolveTenantStore, trackingNamePrefix } from "@/lib/tracking/tenantD1Registry";
45
+ import { persistInteractions } from "@/lib/tracking/interactionStore";
46
+ import type { AnalyticsEngineLike } from "@/lib/activity/store";
47
+
48
+ export const prerender = false;
49
+
50
+ /** Cap the batch so one beacon can't fan out unbounded off-path work. */
51
+ const MAX_BATCH = 50;
52
+
53
+ /**
54
+ * Resolve both sinks and write the batch. Runs PAST the response
55
+ * (`runAfterResponse`), so binding resolution + the D1 REST round-trip never
56
+ * hold the beacon open. Never throws — a telemetry beacon must not fail because
57
+ * storage is unconfigured or unreachable.
58
+ */
59
+ async function storeInteractions(
60
+ tenantId: string,
61
+ events: StampedInteractionEvent[],
62
+ ingestedAt: string,
63
+ ): Promise<void> {
64
+ try {
65
+ const [appsDb, analyticsBinding, control, namePrefix] = await Promise.all([
66
+ readD1("STOREFRONT_APPS_DB"),
67
+ readAnalytics("INTERACTION_ANALYTICS"),
68
+ readD1Control(),
69
+ trackingNamePrefix(),
70
+ ]);
71
+
72
+ // The raw sink needs BOTH the directory (shared apps D1) and the D1 REST
73
+ // credentials that address a per-tenant database; either absent ⇒ skip it.
74
+ const store =
75
+ appsDb && control
76
+ ? await resolveTenantStore(tenantId, {
77
+ apps: fromD1(appsDb),
78
+ control,
79
+ namePrefix,
80
+ at: ingestedAt,
81
+ })
82
+ : undefined;
83
+
84
+ await persistInteractions(events, {
85
+ store,
86
+ analytics: (analyticsBinding as AnalyticsEngineLike | undefined) ?? undefined,
87
+ });
88
+ } catch {
89
+ /* never fail a telemetry beacon */
90
+ }
91
+ }
92
+
93
+ export async function POST(context: APIContext): Promise<Response> {
94
+ try {
95
+ const tenantId = (context.locals as { tenant?: { tenant_id?: string } })?.tenant?.tenant_id;
96
+ // No resolved tenant (e.g. a request that never went through tenant
97
+ // resolution) ⇒ nothing to stamp or store; still 204, never error the beacon.
98
+ if (!tenantId) return new Response(null, { status: 204 });
99
+
100
+ const parsed: unknown = await context.request.json();
101
+ const events = Array.isArray(parsed)
102
+ ? parsed
103
+ : parsed && typeof parsed === "object" && Array.isArray((parsed as { events?: unknown }).events)
104
+ ? (parsed as { events: unknown[] }).events
105
+ : [];
106
+ if (events.length === 0) return new Response(null, { status: 204 });
107
+
108
+ const batch = events.slice(0, MAX_BATCH);
109
+ const ingestedAt = new Date().toISOString();
110
+ const { accepted } = ingestInteractions(batch, tenantId, ingestedAt);
111
+
112
+ if (accepted.length > 0) {
113
+ const cfContext = (context.locals as { cfContext?: CfExecutionContextLike })?.cfContext ?? null;
114
+ runAfterResponse(
115
+ storeInteractions(tenantId, accepted.map((a) => a.event), ingestedAt),
116
+ cfContext,
117
+ );
118
+ }
119
+ } catch {
120
+ /* never fail a telemetry beacon */
121
+ }
122
+ return new Response(null, { status: 204 });
123
+ }
@@ -17,7 +17,7 @@ import TierCards from "@/components/membership/TierCards.astro";
17
17
  import { withBase } from "@/lib/basePath";
18
18
  import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
19
19
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
20
- import { getRegistryService } from "@/lib/apps/adminService.js";
20
+ import { getRegistryService } from "@/lib/apps/registryAccess.js";
21
21
  import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
22
22
  import type { CatalogCollection, CatalogProduct } from "@tot/public-runtime";
23
23
  import {
@@ -42,7 +42,7 @@ import { productLd, breadcrumbLd, reviewLd } from "@/lib/jsonld";
42
42
  import { isUsableImageUrl } from "@/lib/placeholder";
43
43
  import { withBase } from "@/lib/basePath";
44
44
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
45
- import { getRegistryService } from "@/lib/apps/adminService.js";
45
+ import { getRegistryService } from "@/lib/apps/registryAccess.js";
46
46
  import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
47
47
 
48
48
  const { handle } = Astro.params;
@@ -43,6 +43,7 @@ import SiteHeader from "@/components/chrome/SiteHeader.astro";
43
43
  import MegaMenu from "@/components/chrome/MegaMenu.astro";
44
44
  import NavDropdown from "@/components/chrome/NavDropdown.astro";
45
45
  import SiteFooter from "@/components/chrome/SiteFooter.astro";
46
+ import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
46
47
  import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
47
48
  import PactActNotice from "@/components/compliance/PactActNotice.astro";
48
49
  import AdultSignatureNotice from "@/components/compliance/AdultSignatureNotice.astro";
@@ -88,28 +89,33 @@ const { tenant, theme } = Astro.params;
88
89
  const cspNonce = Astro.locals.cspNonce;
89
90
 
90
91
  // --- Sample chrome data (preview-only; no live tenant content) ----------------
92
+ // Every actionable item declares its tracking intent (`action`) alongside its
93
+ // identity (`id`) — the two are different axes and neither is derived from the
94
+ // other. See docs/architecture/interaction-tracking-attribute-contract.md.
91
95
  const sampleNav: ChromeNavItem[] = [
92
- { id: "nav-new", label: "New", href: "#", flag: true, badge: "NEW" },
96
+ { id: "nav-new", action: "nav-new", label: "New", href: "#", flag: true, badge: "NEW" },
93
97
  {
94
98
  id: "nav-shop",
99
+ action: "nav-shop",
95
100
  label: "Shop",
96
101
  href: "#",
97
102
  columns: [
98
- { title: "Category", links: [{ id: "nav-shop-disposables", label: "Disposables", href: "#" }, { id: "nav-shop-eliquid", label: "E-Liquid", href: "#" }, { id: "nav-shop-devices", label: "Devices", href: "#" }] },
99
- { title: "Featured", links: [{ id: "nav-shop-bestsellers", label: "Best Sellers", href: "#" }, { id: "nav-shop-onsale", label: "On Sale", href: "#", badge: "%" }] },
100
- { title: "Brands", links: [{ id: "nav-shop-northwind", label: "Northwind", href: "#" }, { id: "nav-shop-summit", label: "Summit Co.", href: "#" }] },
103
+ { title: "Category", links: [{ id: "nav-shop-disposables", action: "nav-disposables", label: "Disposables", href: "#" }, { id: "nav-shop-eliquid", action: "nav-eliquid", label: "E-Liquid", href: "#" }, { id: "nav-shop-devices", action: "nav-devices", label: "Devices", href: "#" }] },
104
+ { title: "Featured", links: [{ id: "nav-shop-bestsellers", action: "nav-bestsellers", label: "Best Sellers", href: "#" }, { id: "nav-shop-onsale", action: "nav-on-sale", label: "On Sale", href: "#", badge: "%" }] },
105
+ { title: "Brands", links: [{ id: "nav-shop-northwind", action: "nav-brand-northwind", label: "Northwind", href: "#" }, { id: "nav-shop-summit", action: "nav-brand-summit", label: "Summit Co.", href: "#" }] },
101
106
  ],
102
107
  },
103
- { id: "nav-coils", label: "Coils & Parts", href: "#", children: [{ id: "nav-coils-coils", label: "Coils", href: "#" }, { id: "nav-coils-pods", label: "Pods", href: "#" }, { id: "nav-coils-batteries", label: "Batteries", href: "#" }] },
104
- { id: "nav-sale", label: "On Sale", href: "#" },
108
+ { id: "nav-coils", action: "nav-coils-and-parts", label: "Coils & Parts", href: "#", children: [{ id: "nav-coils-coils", action: "nav-coils", label: "Coils", href: "#" }, { id: "nav-coils-pods", action: "nav-pods", label: "Pods", href: "#" }, { id: "nav-coils-batteries", action: "nav-batteries", label: "Batteries", href: "#" }] },
109
+ { id: "nav-sale", action: "nav-on-sale", label: "On Sale", href: "#" },
105
110
  ];
106
111
  const sampleSocial = [
107
112
  { label: "X", href: "#", iconPath: "M18.9 2H22l-7.1 8.1L23 22h-6.6l-5-6.6L5.7 22H2.6l7.6-8.7L1.5 2h6.8l4.5 6 5.1-6z" },
108
113
  ];
109
114
  const sampleFooterCols = [
110
- { title: "Shop", links: [{ id: "ftr-shop-disposables", label: "Disposables", href: "#" }, { id: "ftr-shop-eliquid", label: "E-Liquid", href: "#" }, { id: "ftr-shop-devices", label: "Devices", href: "#" }] },
111
- { title: "Support", links: [{ id: "ftr-support-contact", label: "Contact", href: "#" }, { id: "ftr-support-shipping", label: "Shipping", href: "#" }, { id: "ftr-support-returns", label: "Returns", href: "#" }] },
112
- { title: "Company", links: [{ id: "ftr-company-about", label: "About", href: "#" }, { id: "ftr-company-blog", label: "Blog", href: "#" }] },
115
+ // The footer repeats the header's intents: same `action`, different placement.
116
+ { title: "Shop", links: [{ id: "ftr-shop-disposables", action: "nav-disposables", label: "Disposables", href: "#" }, { id: "ftr-shop-eliquid", action: "nav-eliquid", label: "E-Liquid", href: "#" }, { id: "ftr-shop-devices", action: "nav-devices", label: "Devices", href: "#" }] },
117
+ { title: "Support", links: [{ id: "ftr-support-contact", action: "nav-contact", label: "Contact", href: "#" }, { id: "ftr-support-shipping", action: "nav-shipping", label: "Shipping", href: "#" }, { id: "ftr-support-returns", action: "nav-returns", label: "Returns", href: "#" }] },
118
+ { title: "Company", links: [{ id: "ftr-company-about", action: "nav-about", label: "About", href: "#" }, { id: "ftr-company-blog", action: "nav-blog", label: "Blog", href: "#" }] },
113
119
  ];
114
120
  // BrandGrid samples: text-wordmark tiles and logo-image tiles (Img renders
115
121
  // deterministic placeholders offline so the image code path is exercised
@@ -321,12 +327,17 @@ const sampleManageEntries: SubscriptionManageEntry[] = [
321
327
  // --- Sample islands data (preview-only) ----------------------------------
322
328
  const sampleMobileNav = [
323
329
  {
330
+ id: "mnav-shop",
331
+ action: "nav-shop",
324
332
  label: "Shop",
325
333
  href: "#",
326
- children: [{ label: "Disposables", href: "#" }, { label: "E-Liquid", href: "#" }],
334
+ children: [
335
+ { id: "mnav-shop-disposables", action: "nav-disposables", label: "Disposables", href: "#" },
336
+ { id: "mnav-shop-eliquid", action: "nav-eliquid", label: "E-Liquid", href: "#" },
337
+ ],
327
338
  },
328
- { label: "Coils & Parts", href: "#" },
329
- { label: "On Sale", href: "#" },
339
+ { id: "mnav-coils", action: "nav-coils-and-parts", label: "Coils & Parts", href: "#" },
340
+ { id: "mnav-sale", action: "nav-on-sale", label: "On Sale", href: "#" },
330
341
  ];
331
342
  const sampleFromProduct = sampleProducts.find((p) => p.handle === "sample-from")!;
332
343
  const sampleRecentlyViewed: RVItem[] = [
@@ -1149,7 +1160,7 @@ const componentVarsInline = Object.entries(componentVars)
1149
1160
  <section class="sg-block">
1150
1161
  <p class="sg-h">Chrome — UtilityNav</p>
1151
1162
  <UtilityNav
1152
- links={[{ id: "util-news", label: "News", href: "#" }, { id: "util-about", label: "About", href: "#" }, { id: "util-contact", label: "Contact", href: "#" }]}
1163
+ links={[{ id: "util-news", action: "nav-news", label: "News", href: "#" }, { id: "util-about", action: "nav-about", label: "About", href: "#" }, { id: "util-contact", action: "nav-contact", label: "Contact", href: "#" }]}
1153
1164
  locale="English"
1154
1165
  currency="United States (USD $)"
1155
1166
  social={sampleSocial}
@@ -1181,7 +1192,15 @@ const componentVarsInline = Object.entries(componentVars)
1181
1192
  <p class="sg-h">Chrome — NavDropdown accordion (mobile mega parity, zero-JS)</p>
1182
1193
  <div style="max-width:18rem;background:var(--color-surface);border:1px solid var(--color-border);border-radius:8px;padding:8px 12px">
1183
1194
  <ul class="grid">
1184
- {sampleNav.map((item) => <NavDropdown item={item} mode="accordion" />)}
1195
+ {sampleNav.map((item, i) => (
1196
+ <NavDropdown
1197
+ item={item}
1198
+ mode="accordion"
1199
+ placement={CHROME_PLACEMENT.mobileMenu}
1200
+ menuPlacement={CHROME_PLACEMENT.mobileMenu}
1201
+ index={i}
1202
+ />
1203
+ ))}
1185
1204
  </ul>
1186
1205
  </div>
1187
1206
  <p class="form-help" style="margin-top:8px;color:var(--color-muted)">Same nav items in the mobile menu: mega columns become inline <code>&lt;details&gt;</code> accordions (keyboard + touch operable). Toggle a caret item to expand.</p>
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The scheduled Cloudflare Worker entry for `dt-prune-and-rollup`.
3
+ *
4
+ * A SEPARATE, DEDICATED Worker script from the Astro-adapted storefront
5
+ * Worker — deliberately, not a `scheduled` export bolted onto the Astro
6
+ * entry. The @astrojs/cloudflare adapter OWNS `dist/server/entry.mjs` and
7
+ * bakes its target environment in at BUILD time via `CLOUDFLARE_ENV`
8
+ * (`wrangler.toml`'s own "DEPLOY FOOTGUN" comment); the documented way to add
9
+ * a non-`fetch` handler there is a custom entrypoint that re-imports the
10
+ * adapter's generated `handle()`, which would make this cron trigger's
11
+ * correctness depend on the SAME footgun-prone build/deploy sequencing as
12
+ * the whole storefront. This module instead has nothing to do with Astro at
13
+ * all — it is a plain `ExportedHandler` deployed under its own Worker name
14
+ * (`wrangler.prune-rollup.toml`, `[triggers]` below), reusing the tracking
15
+ * library's already-built, framework-agnostic pieces
16
+ * (`lib/tracking/pruneRollup.ts`, `d1Control.ts`, `interactionStore.ts`).
17
+ * There is no other scheduled-Worker precedent in this repo to follow; this
18
+ * is the first one, chosen to keep the footgun-prone Astro deploy path
19
+ * completely untouched.
20
+ *
21
+ * BINDINGS (declared in `wrangler.prune-rollup.toml`, not this file):
22
+ * - `STOREFRONT_APPS_DB` — the SAME shared D1 database the main storefront
23
+ * Worker binds, for the `tracking_tenant_databases` directory.
24
+ * - `INTERACTION_ANALYTICS` — the SAME Analytics Engine dataset the main
25
+ * Worker writes rollups to.
26
+ * - `CF_ACCOUNT_ID` / `TRACKING_D1_API_TOKEN` (secret) — D1 REST
27
+ * credentials for reaching each tenant's OWN database (Worker bindings
28
+ * are static; a per-tenant database can only be addressed over the D1
29
+ * HTTP API, same reasoning as `d1Control.ts`'s module doc).
30
+ * - `TRACKING_D1_NAME_PREFIX` (optional) — unused by this worker directly,
31
+ * kept only for parity/documentation; the prune sweep addresses tenants
32
+ * by `database_uuid` from the directory, never by re-deriving the name.
33
+ */
34
+ import type { Queryable } from "../lib/d1/catalog.js";
35
+ import type { D1Like } from "../lib/d1/catalog.js";
36
+ import { fromD1 } from "../lib/d1/catalog.js";
37
+ import type { AnalyticsEngineLike } from "../lib/activity/store.js";
38
+ import type { D1ControlConfig } from "../lib/tracking/d1Control.js";
39
+ import { runPruneSweep, type PruneSweepResult } from "../lib/tracking/pruneRollup.js";
40
+
41
+ /** The env bindings this Worker needs — a local, minimal slice (this repo
42
+ * does not take a `@cloudflare/workers-types` dependency; see `d1Control.ts`
43
+ * and `activity/store.ts` for the same convention). */
44
+ export interface PruneRollupEnv {
45
+ STOREFRONT_APPS_DB: D1Like;
46
+ INTERACTION_ANALYTICS?: AnalyticsEngineLike;
47
+ CF_ACCOUNT_ID?: string;
48
+ TRACKING_D1_API_TOKEN?: string;
49
+ /** Tenants processed per invocation; overridable for a slower/faster cron. */
50
+ PRUNE_BATCH_SIZE?: string;
51
+ }
52
+
53
+ /** The minimal slice of Cloudflare's `ScheduledController` this handler uses. */
54
+ export interface ScheduledControllerLike {
55
+ scheduledTime: number;
56
+ }
57
+
58
+ /** The minimal slice of Cloudflare's `ExecutionContext` this handler uses. */
59
+ export interface ExecutionContextLike {
60
+ waitUntil(promise: Promise<unknown>): void;
61
+ }
62
+
63
+ /** Build the D1 REST control config from env, or undefined when unconfigured
64
+ * (mirrors `d1Control.ts`'s `readD1Control()` "skip, never fail" posture). */
65
+ function controlFromEnv(env: PruneRollupEnv): D1ControlConfig | undefined {
66
+ if (!env.CF_ACCOUNT_ID || !env.TRACKING_D1_API_TOKEN) return undefined;
67
+ return { accountId: env.CF_ACCOUNT_ID, apiToken: env.TRACKING_D1_API_TOKEN };
68
+ }
69
+
70
+ /**
71
+ * The pure orchestration the `scheduled` export below delegates to — kept
72
+ * separate so it is callable/testable without a real `ScheduledController`.
73
+ * Never throws: a misconfigured environment (no D1 REST credentials) logs
74
+ * and no-ops rather than failing the cron invocation.
75
+ */
76
+ export async function handleScheduledPrune(
77
+ env: PruneRollupEnv,
78
+ now: string = new Date().toISOString(),
79
+ ): Promise<PruneSweepResult | undefined> {
80
+ const control = controlFromEnv(env);
81
+ if (!control) {
82
+ console.error(
83
+ "[tracking:prune] skipped — CF_ACCOUNT_ID/TRACKING_D1_API_TOKEN not configured",
84
+ );
85
+ return undefined;
86
+ }
87
+
88
+ const apps: Queryable = fromD1(env.STOREFRONT_APPS_DB);
89
+ const batchSize = env.PRUNE_BATCH_SIZE ? Number.parseInt(env.PRUNE_BATCH_SIZE, 10) : undefined;
90
+
91
+ const result = await runPruneSweep({
92
+ apps,
93
+ control,
94
+ analytics: env.INTERACTION_ANALYTICS,
95
+ now,
96
+ ...(batchSize && Number.isFinite(batchSize) ? { batchSize } : {}),
97
+ });
98
+
99
+ console.info(
100
+ `[tracking:prune] swept ${result.tenantsSwept} tenant(s), pruned ${result.totalPrunedRows} row(s), ` +
101
+ `wrote ${result.totalAggregatesWritten} aggregate(s)`,
102
+ );
103
+ return result;
104
+ }
105
+
106
+ export default {
107
+ async scheduled(
108
+ controller: ScheduledControllerLike,
109
+ env: PruneRollupEnv,
110
+ ctx: ExecutionContextLike,
111
+ ): Promise<void> {
112
+ ctx.waitUntil(
113
+ handleScheduledPrune(env, new Date(controller.scheduledTime).toISOString()).catch((err) => {
114
+ console.error(
115
+ `[tracking:prune] sweep threw: ${err instanceof Error ? err.message : String(err)}`,
116
+ );
117
+ }),
118
+ );
119
+ },
120
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsc" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../typescript/bin/tsc" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../typescript/bin/tsc" "$@"
40
+ else
41
+ exec node "$basedir/../typescript/bin/tsc" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/typescript/bin/tsc
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsserver" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../typescript/bin/tsserver" "$@"
40
+ else
41
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/typescript/bin/tsserver
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../vitest/vitest.mjs" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../vitest/vitest.mjs" "$@"
40
+ else
41
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/vitest/vitest.mjs
@@ -28,6 +28,7 @@
28
28
  * `parseConfig.ts` re-export from here so every existing renderer import
29
29
  * keeps resolving unchanged.
30
30
  */
31
+ import { validateTracking, type TrackingDeclaration } from "./interaction-tracking.js";
31
32
 
32
33
  /**
33
34
  * A navigation/footer/CTA link. `id` is a STABLE identifier, independent of
@@ -35,14 +36,20 @@
35
36
  * registry (spec: no runtime registry, no DOM observer; just a schema-
36
37
  * validated identifier + a real `href` fallback, always present on every
37
38
  * actionable item). `external` opens off-site; `badge` shows e.g. "NEW".
39
+ *
40
+ * Every actionable item also carries the declarative-interaction-tracking
41
+ * declaration ({@link TrackingDeclaration}): a stable `action` naming the
42
+ * INTENT, or `tracking: "none"` to stamp nothing at all. `id` (identity) and
43
+ * `action` (intent) are different axes and are never derived from each other —
44
+ * see docs/architecture/interaction-tracking-attribute-contract.md.
38
45
  */
39
- export interface ChromeLink {
46
+ export type ChromeLink = {
40
47
  id: string;
41
48
  label: string;
42
49
  href: string;
43
50
  external?: boolean;
44
51
  badge?: string;
45
- }
52
+ } & TrackingDeclaration;
46
53
 
47
54
  /** One column of a mega-menu panel. */
48
55
  export interface MegaColumn {
@@ -56,7 +63,7 @@ export interface MegaColumn {
56
63
  * marks it (e.g. "NEW"); `flag` gives it the emphasized/highlighted
57
64
  * treatment. See {@link ChromeLink} for `id`.
58
65
  */
59
- export interface ChromeNavItem {
66
+ export type ChromeNavItem = {
60
67
  id: string;
61
68
  label: string;
62
69
  href: string;
@@ -64,7 +71,7 @@ export interface ChromeNavItem {
64
71
  flag?: boolean;
65
72
  children?: ChromeLink[];
66
73
  columns?: MegaColumn[];
67
- }
74
+ } & TrackingDeclaration;
68
75
 
69
76
  export interface SocialLink {
70
77
  label: string;
@@ -81,16 +88,16 @@ export interface ChromeLogo {
81
88
  }
82
89
 
83
90
  /** A marketing conversion CTA (header-actions / footer, non-commerce tenants). */
84
- export interface ChromeCta {
91
+ export type ChromeCta = {
85
92
  id: string;
86
93
  label: string;
87
94
  href: string;
88
95
  variant?: "primary" | "secondary" | "ghost";
89
- }
96
+ } & TrackingDeclaration;
90
97
 
91
98
  /** A stable link-with-a-name-and-href, minus badge/external — used for the
92
99
  * small number of single-purpose action links (e.g. a member-price cue). */
93
- export type ChromeActionLink = Pick<ChromeLink, "id" | "label" | "href">;
100
+ export type ChromeActionLink = { id: string; label: string; href: string } & TrackingDeclaration;
94
101
 
95
102
  export interface FooterColumn {
96
103
  title: string;
@@ -371,6 +378,7 @@ function validateLink(v: unknown, path: string, errors: string[]): v is ChromeLi
371
378
  if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
372
379
  checkOptionalBoolean(v.external, `${path}.external`, errors);
373
380
  checkOptionalString(v.badge, `${path}.badge`, errors);
381
+ validateTracking(v, path, errors);
374
382
  return errors.length === before;
375
383
  }
376
384
 
@@ -384,6 +392,7 @@ function validateActionLink(v: unknown, path: string, errors: string[]): boolean
384
392
  if (!isNonEmptyString(v.id)) errors.push(`${path}.id: required, non-empty string`);
385
393
  if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
386
394
  if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
395
+ validateTracking(v, path, errors);
387
396
  return errors.length === before;
388
397
  }
389
398
 
@@ -415,6 +424,7 @@ function validateNavItem(v: unknown, path: string, errors: string[]): v is Chrom
415
424
  if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
416
425
  checkOptionalString(v.badge, `${path}.badge`, errors);
417
426
  checkOptionalBoolean(v.flag, `${path}.flag`, errors);
427
+ validateTracking(v, path, errors);
418
428
  if (v.children !== undefined) {
419
429
  if (!Array.isArray(v.children)) errors.push(`${path}.children: must be an array`);
420
430
  else v.children.forEach((c, i) => validateLink(c, `${path}.children[${i}]`, errors));
@@ -438,6 +448,7 @@ function validateCta(v: unknown, path: string, errors: string[]): v is ChromeCta
438
448
  if (v.variant !== undefined && !["primary", "secondary", "ghost"].includes(v.variant as string)) {
439
449
  errors.push(`${path}.variant: must be one of primary, secondary, ghost`);
440
450
  }
451
+ validateTracking(v, path, errors);
441
452
  return errors.length === before;
442
453
  }
443
454
 
@@ -33,6 +33,16 @@ export * from "./customization-versioning.js";
33
33
  export * from "./customization-scripts.js";
34
34
  export * from "./customization-reconcile.js";
35
35
  export * from "./chrome.js";
36
+ // Declarative interaction tracking: the data-tot-el / -action / -placement
37
+ // attribute contract, its single emit path, and the ONE delegated click
38
+ // listener both document mounts install. Reads declared attributes only —
39
+ // never rendered content, never form values.
40
+ // See docs/architecture/interaction-tracking-attribute-contract.md.
41
+ export * from "./interaction-tracking.js";
42
+ // `data-tot-el` id derivation: the authoring-time engine that proposes a
43
+ // stable, human-readable IDENTITY from fragment STRUCTURE alone. A proposal
44
+ // engine only — never a runtime fallback, and it derives no `action`.
45
+ export * from "./tracking-id-derivation.js";
36
46
  export * from "./candidate-index.js";
37
47
  export * from "./extension-contract.js";
38
48
  export * from "./integration-capabilities.js";
@@ -65,6 +65,7 @@ export * from "./consent-broker.js";
65
65
  export * from "./consent-cmp-demand.js";
66
66
  export * from "./consent-config.js";
67
67
  export * from "./consent-preview.js";
68
+ export * from "./interaction-tracking-gate.js";
68
69
  export * from "./candidate-evidence.js";
69
70
  export * from "./lifecycle.js";
70
71
  export * from "./lifecycle-runtime.js";