@tokenoftrust/storefront-runner 1.4.2-rc.0 → 2.0.0

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 (363) hide show
  1. package/apps/storefront/astro.config.mjs +3 -3
  2. package/apps/storefront/env.d.ts +54 -5
  3. package/apps/storefront/integrations/materialize-guard.mjs +12 -14
  4. package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
  5. package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
  6. package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
  7. package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
  8. package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
  9. package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
  10. package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
  11. package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
  12. package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
  13. package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
  14. package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
  15. package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
  16. package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
  17. package/apps/storefront/migrations-apps/meta/_journal.json +42 -0
  18. package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
  19. package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
  20. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
  21. package/apps/storefront/public/shared/commerce-chrome.css +20 -2
  22. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
  23. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
  24. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
  25. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
  26. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
  27. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
  28. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
  29. package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
  30. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
  31. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
  32. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
  33. package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
  34. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
  35. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
  36. package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
  37. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
  38. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
  39. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
  40. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
  41. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
  42. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
  43. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
  44. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
  45. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
  46. package/apps/storefront/src/components/Img.astro +1 -1
  47. package/apps/storefront/src/components/admin/AdminBlogTab.astro +19 -16
  48. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
  49. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1057 -412
  50. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  51. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
  52. package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
  53. package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
  54. package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
  55. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
  56. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
  57. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
  58. package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
  59. package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
  60. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
  61. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
  62. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
  63. package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
  64. package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
  65. package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
  66. package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
  67. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
  68. package/apps/storefront/src/components/membership/TierCards.astro +2 -2
  69. package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
  70. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
  71. package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
  72. package/apps/storefront/src/config/devTenantSeed.ts +9 -9
  73. package/apps/storefront/src/config/resolver.ts +4 -4
  74. package/apps/storefront/src/layouts/Layout.astro +100 -20
  75. package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
  76. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +86 -6
  77. package/apps/storefront/src/lib/activity/deployVersion.ts +1 -1
  78. package/apps/storefront/src/lib/admin/adminShell.ts +2 -0
  79. package/apps/storefront/src/lib/admin/envelope.ts +1 -2
  80. package/apps/storefront/src/lib/apps/adminService.ts +10 -0
  81. package/apps/storefront/src/lib/apps/adminSession.ts +1 -1
  82. package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
  83. package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
  84. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
  85. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
  86. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
  87. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
  88. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
  89. package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
  90. package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
  91. package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
  92. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
  93. package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
  94. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +8 -0
  95. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +1 -1
  96. package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
  97. package/apps/storefront/src/lib/apps/registryService.ts +264 -10
  98. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
  99. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
  100. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
  101. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
  102. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
  103. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
  104. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
  105. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
  106. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
  107. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
  108. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
  109. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
  110. package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
  111. package/apps/storefront/src/lib/auth/adminEntry.ts +2 -2
  112. package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
  113. package/apps/storefront/src/lib/blog/collection.ts +97 -10
  114. package/apps/storefront/src/lib/blog/markdown.ts +1 -1
  115. package/apps/storefront/src/lib/blog/provider.ts +57 -3
  116. package/apps/storefront/src/lib/blog/types.ts +9 -9
  117. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
  118. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
  119. package/apps/storefront/src/lib/chrome/model.ts +48 -253
  120. package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
  121. package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
  122. package/apps/storefront/src/lib/chrome/variant.ts +39 -0
  123. package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
  124. package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
  125. package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
  126. package/apps/storefront/src/lib/content/index.ts +5 -5
  127. package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
  128. package/apps/storefront/src/lib/content-edit/route.ts +1 -1
  129. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  130. package/apps/storefront/src/lib/d1/schema-apps.ts +276 -0
  131. package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
  132. package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
  133. package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
  134. package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
  135. package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
  136. package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
  137. package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
  138. package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
  139. package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
  140. package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
  141. package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
  142. package/apps/storefront/src/lib/edgeCache.ts +18 -6
  143. package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
  144. package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
  145. package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
  146. package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
  147. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
  148. package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
  149. package/apps/storefront/src/lib/membership/model.ts +1 -2
  150. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +2 -2
  151. package/apps/storefront/src/lib/newsletter/consent.ts +2 -2
  152. package/apps/storefront/src/lib/newsletter/send.ts +3 -3
  153. package/apps/storefront/src/lib/previewReload.ts +57 -1
  154. package/apps/storefront/src/lib/publish/adminPublishTab.ts +1775 -669
  155. package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
  156. package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
  157. package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
  158. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
  159. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
  160. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
  161. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
  162. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
  163. package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
  164. package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
  165. package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
  166. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
  167. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
  168. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
  169. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
  170. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
  171. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
  172. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
  173. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
  174. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
  175. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
  176. package/apps/storefront/src/lib/publish/shipWorkspace.ts +22 -3
  177. package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
  178. package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
  179. package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
  180. package/apps/storefront/src/lib/rawChrome.ts +125 -47
  181. package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
  182. package/apps/storefront/src/lib/seo/alternates.ts +1 -1
  183. package/apps/storefront/src/lib/social/notify.ts +4 -4
  184. package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
  185. package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
  186. package/apps/storefront/src/lib/subscription/model.ts +4 -4
  187. package/apps/storefront/src/lib/the-build/provider.ts +9 -4
  188. package/apps/storefront/src/middleware/index.ts +273 -51
  189. package/apps/storefront/src/pages/[...slug].astro +9 -4
  190. package/apps/storefront/src/pages/admin/review-release.astro +265 -0
  191. package/apps/storefront/src/pages/admin/select.astro +96 -0
  192. package/apps/storefront/src/pages/admin.astro +16 -2
  193. package/apps/storefront/src/pages/api/activity.ts +8 -8
  194. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
  195. package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
  196. package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
  197. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
  198. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
  199. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
  200. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
  201. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
  202. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
  203. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
  204. package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
  205. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
  206. package/apps/storefront/src/pages/api/rum/vitals.ts +1 -2
  207. package/apps/storefront/src/pages/blog/[slug].astro +5 -5
  208. package/apps/storefront/src/pages/blog/author/[author].astro +3 -3
  209. package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
  210. package/apps/storefront/src/pages/blog/index.astro +2 -2
  211. package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
  212. package/apps/storefront/src/pages/cockpit.astro +61 -1362
  213. package/apps/storefront/src/pages/index.astro +10 -5
  214. package/apps/storefront/src/pages/llms.txt.ts +1 -1
  215. package/apps/storefront/src/pages/products/[handle].astro +7 -6
  216. package/apps/storefront/src/pages/sitemap.xml.ts +2 -2
  217. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
  218. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
  219. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
  220. package/apps/storefront/src/pages/the-build/[slug].astro +4 -4
  221. package/apps/storefront/src/pages/the-build/index.astro +2 -2
  222. package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
  223. package/apps/storefront/src/styles/admin.css +27 -1
  224. package/apps/storefront/tsconfig.json +1 -1
  225. package/package.json +1 -1
  226. package/packages/public-runtime/package.json +2 -1
  227. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
  228. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
  229. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
  230. package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
  231. package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
  232. package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
  233. package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
  234. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
  235. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
  236. package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
  237. package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
  238. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
  239. package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
  240. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
  241. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
  242. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
  243. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
  244. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
  245. package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
  246. package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
  247. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
  248. package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
  249. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
  250. package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
  251. package/packages/public-runtime/src/activity/catalog.ts +41 -37
  252. package/packages/public-runtime/src/activity/event.ts +6 -6
  253. package/packages/public-runtime/src/activity/index.ts +2 -2
  254. package/packages/public-runtime/src/activity/redaction.ts +4 -4
  255. package/packages/public-runtime/src/candidate-index.ts +42 -8
  256. package/packages/public-runtime/src/checkout-orders.ts +2 -2
  257. package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
  258. package/packages/public-runtime/src/checkout-style.ts +7 -7
  259. package/packages/public-runtime/src/checkout.ts +3 -3
  260. package/packages/public-runtime/src/chrome.ts +777 -0
  261. package/packages/public-runtime/src/compliance-obligations.ts +310 -0
  262. package/packages/public-runtime/src/csp.ts +99 -0
  263. package/packages/public-runtime/src/customization-runtime.ts +32 -1
  264. package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
  265. package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
  266. package/packages/public-runtime/src/embed-catalog.mjs +40 -0
  267. package/packages/public-runtime/src/evidence.ts +288 -0
  268. package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
  269. package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
  270. package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
  271. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
  272. package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
  273. package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
  274. package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
  275. package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
  276. package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
  277. package/packages/public-runtime/src/index.ts +16 -1
  278. package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
  279. package/packages/public-runtime/src/integration/assurance.ts +228 -0
  280. package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
  281. package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
  282. package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
  283. package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
  284. package/packages/public-runtime/src/integration/consent-config.ts +260 -0
  285. package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
  286. package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
  287. package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
  288. package/packages/public-runtime/src/integration/csp.ts +105 -0
  289. package/packages/public-runtime/src/integration/digest.ts +43 -0
  290. package/packages/public-runtime/src/integration/environment.ts +228 -0
  291. package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
  292. package/packages/public-runtime/src/integration/events.ts +701 -0
  293. package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
  294. package/packages/public-runtime/src/integration/index.ts +90 -0
  295. package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
  296. package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
  297. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
  298. package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
  299. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
  300. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
  301. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
  302. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
  303. package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
  304. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
  305. package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
  306. package/packages/public-runtime/src/integration/manifest.ts +470 -0
  307. package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
  308. package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
  309. package/packages/public-runtime/src/integration/observability.ts +451 -0
  310. package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
  311. package/packages/public-runtime/src/integration/phases.ts +182 -0
  312. package/packages/public-runtime/src/integration/plan.ts +112 -0
  313. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
  314. package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
  315. package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
  316. package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
  317. package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
  318. package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
  319. package/packages/public-runtime/src/integration/render-phases.ts +267 -0
  320. package/packages/public-runtime/src/integration/resolve.ts +465 -0
  321. package/packages/public-runtime/src/integration/result.ts +239 -0
  322. package/packages/public-runtime/src/integration/secrets.ts +92 -0
  323. package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
  324. package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
  325. package/packages/public-runtime/src/integration/version.ts +92 -0
  326. package/packages/public-runtime/src/integration-capabilities.ts +520 -0
  327. package/packages/public-runtime/src/membership.ts +2 -2
  328. package/packages/public-runtime/src/raw-html-policy.ts +2 -4
  329. package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
  330. package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
  331. package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
  332. package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
  333. package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
  334. package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
  335. package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
  336. package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
  337. package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
  338. package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
  339. package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
  340. package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
  341. package/packages/public-runtime/src/static-bundle.ts +5 -5
  342. package/packages/public-runtime/src/tenant-assets.ts +143 -4
  343. package/packages/public-runtime/src/tenant.ts +45 -9
  344. package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
  345. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
  346. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
  347. package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
  348. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
  349. package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
  350. package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
  351. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
  352. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
  353. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
  354. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
  355. package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
  356. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
  357. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
  358. package/scripts/dev/file-browser.mjs +2 -2
  359. package/scripts/dev/file-writer.mjs +2 -2
  360. package/scripts/dev/integration-guardrails.mjs +498 -0
  361. package/scripts/dev/safe-path.mjs +2 -2
  362. package/scripts/dev/tenant-source-root.mjs +1 -1
  363. package/scripts/tot-dev.mjs +2 -2
@@ -0,0 +1,777 @@
1
+ /**
2
+ * Chrome model — the ONE schema for a tenant's site chrome (header + footer),
3
+ * plus its fail-closed validator.
4
+ *
5
+ * Consolidates what used to be two competing, undocumented shapes:
6
+ * - a lean `SiteChrome` (lib/storyblok/content-model.ts), consumed by the
7
+ * token-styled <SiteHeader>/<SiteFooter> components via
8
+ * Astro.locals.content.getChrome();
9
+ * - a superset `TenantChrome`, consumed by the raw `chrome.html` splice
10
+ * wrapper (lib/rawChrome.ts) via lib/chrome/parseConfig.ts, gated on an
11
+ * explicit `sharedChrome: true` flag.
12
+ * Both read the same `tenants/<id>/content/chrome.json` file; neither was the
13
+ * complete picture. This file is now the single source of truth for that
14
+ * file's shape — both consumers map from `ChromeConfig`.
15
+ *
16
+ * `header` and `footer` are separate, independently-evolvable sub-shapes: a
17
+ * change to one's fields never forces a change to the other's.
18
+ *
19
+ * Lives in `@tot/public-runtime` (not the storefront renderer's `lib/chrome/`)
20
+ * so `@tot/private-controlplane`'s write-side `ArtifactValidator` for
21
+ * `tenants/<id>/content/chrome.json`
22
+ * (packages/private-controlplane/src/customization-chrome.ts) can call
23
+ * {@link validateChromeConfig} without the renderer importing the
24
+ * control-plane package — the `renderer-no-controlplane` fence
25
+ * (apps/storefront/.dependency-cruiser.cjs) runs the other way: packages may
26
+ * depend on `@tot/public-runtime`, but the renderer must never import
27
+ * `@tot/private-controlplane`. `apps/storefront/src/lib/chrome/model.ts` and
28
+ * `parseConfig.ts` re-export from here so every existing renderer import
29
+ * keeps resolving unchanged.
30
+ */
31
+
32
+ /**
33
+ * A navigation/footer/CTA link. `id` is a STABLE identifier, independent of
34
+ * the visible `label` copy — the resolved substitute for a governed action
35
+ * registry (spec: no runtime registry, no DOM observer; just a schema-
36
+ * validated identifier + a real `href` fallback, always present on every
37
+ * actionable item). `external` opens off-site; `badge` shows e.g. "NEW".
38
+ */
39
+ export interface ChromeLink {
40
+ id: string;
41
+ label: string;
42
+ href: string;
43
+ external?: boolean;
44
+ badge?: string;
45
+ }
46
+
47
+ /** One column of a mega-menu panel. */
48
+ export interface MegaColumn {
49
+ title?: string;
50
+ links: ChromeLink[];
51
+ }
52
+
53
+ /**
54
+ * A primary-nav item. A plain link (no children/columns), a single-column
55
+ * dropdown (`children`), or a multi-column mega-menu (`columns`). `badge`
56
+ * marks it (e.g. "NEW"); `flag` gives it the emphasized/highlighted
57
+ * treatment. See {@link ChromeLink} for `id`.
58
+ */
59
+ export interface ChromeNavItem {
60
+ id: string;
61
+ label: string;
62
+ href: string;
63
+ badge?: string;
64
+ flag?: boolean;
65
+ children?: ChromeLink[];
66
+ columns?: MegaColumn[];
67
+ }
68
+
69
+ export interface SocialLink {
70
+ label: string;
71
+ href: string;
72
+ /** Optional inline-SVG path data (single <path d="…">) — no external images. */
73
+ iconPath?: string;
74
+ }
75
+
76
+ export interface ChromeLogo {
77
+ src: string;
78
+ alt: string;
79
+ width?: number;
80
+ height?: number;
81
+ }
82
+
83
+ /** A marketing conversion CTA (header-actions / footer, non-commerce tenants). */
84
+ export interface ChromeCta {
85
+ id: string;
86
+ label: string;
87
+ href: string;
88
+ variant?: "primary" | "secondary" | "ghost";
89
+ }
90
+
91
+ /** A stable link-with-a-name-and-href, minus badge/external — used for the
92
+ * small number of single-purpose action links (e.g. a member-price cue). */
93
+ export type ChromeActionLink = Pick<ChromeLink, "id" | "label" | "href">;
94
+
95
+ export interface FooterColumn {
96
+ title: string;
97
+ links: ChromeLink[];
98
+ }
99
+
100
+ export interface FooterNewsletter {
101
+ title?: string;
102
+ body?: string;
103
+ /** Form action (defaults to /api/newsletter). */
104
+ action?: string;
105
+ placeholder?: string;
106
+ /** Hidden `source` field value (e.g. "footer"). */
107
+ source?: string;
108
+ /** When true, render the "I confirm I am 21+" age-attestation checkbox (regulated tenants). */
109
+ ageAttestation?: boolean;
110
+ }
111
+
112
+ /** A brand identity: an image wordmark/logo, OR a text wordmark of styled spans. */
113
+ export interface ChromeBrand {
114
+ /** Link target (defaults to "/"). */
115
+ href?: string;
116
+ /** Accessible name (aria-label / img alt), e.g. "Prime Vapor Club". */
117
+ label: string;
118
+ /** Image wordmark. Preferred when present and a valid src. */
119
+ logo?: ChromeLogo;
120
+ /**
121
+ * Text wordmark rendered as inline spans (e.g. PVC's `Prime`/`Vapor`/`Club`),
122
+ * each span optionally given a class the theme styles. Used when there's no
123
+ * logo image (placeholder brand).
124
+ */
125
+ wordmark?: { text: string; className?: string }[];
126
+ /** Marks the brand as a stand-in for a pending real logo (adds data-placeholder). */
127
+ placeholder?: boolean;
128
+ }
129
+
130
+ /** Header search affordance (progressive GET form). Omit to hide search. */
131
+ export interface ChromeSearch {
132
+ /** Form action (defaults to "/search"). */
133
+ action?: string;
134
+ placeholder?: string;
135
+ /** sr-only label (defaults to "Search"). */
136
+ label?: string;
137
+ }
138
+
139
+ /** The thin utility bar above the header: minor links, locale chips, social, cue. */
140
+ export interface ChromeUtilityNav {
141
+ links?: ChromeLink[];
142
+ /** Locale/currency chips shown on the right (e.g. "English", "United States (USD $)"). */
143
+ locale?: string[];
144
+ social?: SocialLink[];
145
+ /** Optional right-aligned member-price cue link. */
146
+ memberCue?: ChromeActionLink;
147
+ }
148
+
149
+ /**
150
+ * Announcement banner. Three variants map to the shared stylesheet's classes:
151
+ * - "marquee": infinite horizontal scroll;
152
+ * - "bar": dismissible + optionally message-rotating promo bar;
153
+ * - "solid": a static single-line solid bar.
154
+ */
155
+ export interface ChromeAnnouncement {
156
+ variant: "marquee" | "bar" | "solid";
157
+ /** One or more messages. "bar" rotates through them when >1 and rotateMs set. */
158
+ messages: string[];
159
+ /** aria-label for the region (defaults to the first message). */
160
+ label?: string;
161
+ /** "bar" only — show a dismiss button (remembered via storageKey). */
162
+ dismissible?: boolean;
163
+ /** "bar" only — rotate messages every N ms (needs messages.length > 1). */
164
+ rotateMs?: number;
165
+ /** localStorage key remembering a dismissal (defaults to a stable derived key). */
166
+ storageKey?: string;
167
+ }
168
+
169
+ /**
170
+ * 21+ age-gate interstitial config (regulated tenants). The raw-chrome renderer
171
+ * (rawChrome.ts) embeds this as JSON; the shared behavior JS mounts the gate inside
172
+ * a closed shadow root — the same tamper-resistance pattern
173
+ * components/islands/IsolatedAgeGate.tsx uses for component pages — so tenant
174
+ * CSS/JS can't reach the dialog.
175
+ */
176
+ export interface ChromeAgeGate {
177
+ minAge: number;
178
+ /** localStorage key remembering a pass (defaults to a stable derived key). */
179
+ storageKey?: string;
180
+ title?: string;
181
+ lede?: string;
182
+ yesLabel?: string;
183
+ noLabel?: string;
184
+ denyText?: string;
185
+ /** Fine-print warning under the actions (e.g. the FDA nicotine line). */
186
+ fineText?: string;
187
+ /** Optional styled wordmark above the title (spans, like ChromeBrand.wordmark). */
188
+ mark?: { text: string; className?: string }[];
189
+ }
190
+
191
+ /** Footer brand block: identity + tagline/contact + optional inline newsletter. */
192
+ export interface ChromeFooterBrand {
193
+ logo?: ChromeLogo;
194
+ wordmark?: { text: string; className?: string }[];
195
+ /** Tagline / contact HTML-free text (rendered with <br> on newlines). */
196
+ text?: string;
197
+ }
198
+
199
+ /** A footer trust mark: a small inline-SVG cue + label (e.g. "Secure checkout"). */
200
+ export interface ChromeTrustMark {
201
+ /** Inner SVG markup (paths/rects/circles), viewBox 0 0 24 24. */
202
+ icon: string;
203
+ label: string;
204
+ }
205
+
206
+ /** Per-tenant <head> extras the wrapper links (the shared css is ALWAYS linked). */
207
+ export interface ChromeHead {
208
+ /** Favicon href (image/png). */
209
+ favicon?: string;
210
+ /** Tenant theme stylesheet hrefs (token definitions), e.g. the *-navy.css theme. */
211
+ themeStylesheets?: string[];
212
+ /** <meta name="theme-color"> — a deliberately brand-fixed surface color. */
213
+ themeColor?: string;
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Header / footer variants — explicit, schema-validated content shapes. A
218
+ // page declares which it wants (see Layout.astro's headerVariant/footerVariant
219
+ // props); NEVER inferred from route/CSS/title. Extend the union to add a
220
+ // variant — additive, non-breaking for content already authored against the
221
+ // existing members.
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /** The only two header content shapes: a full header, or a minimal one
225
+ * (e.g. checkout/policy pages — brand + a reduced action set, no full nav). */
226
+ export const HEADER_VARIANTS = ["primary", "minimal"] as const;
227
+ export type HeaderVariant = (typeof HEADER_VARIANTS)[number];
228
+
229
+ /** Footer content shapes. One member today; the type is an extensible string
230
+ * union (not a boolean) so a future variant is an additive change. */
231
+ export const FOOTER_VARIANTS = ["default"] as const;
232
+ export type FooterVariant = (typeof FOOTER_VARIANTS)[number];
233
+
234
+ export function isHeaderVariant(v: unknown): v is HeaderVariant {
235
+ return typeof v === "string" && (HEADER_VARIANTS as readonly string[]).includes(v);
236
+ }
237
+
238
+ export function isFooterVariant(v: unknown): v is FooterVariant {
239
+ return typeof v === "string" && (FOOTER_VARIANTS as readonly string[]).includes(v);
240
+ }
241
+
242
+ /**
243
+ * Thrown by the renderer's `assertHeaderVariant` / `assertFooterVariant`
244
+ * (apps/storefront/src/lib/chrome/variant.ts) when a page passes an unknown
245
+ * variant. Fails closed
246
+ * at validation time with an actionable message rather than silently falling
247
+ * back to a default — an unrecognized variant is a bug in the caller, not a
248
+ * degraded-but-safe state.
249
+ */
250
+ export class InvalidChromeVariantError extends Error {
251
+ constructor(kind: "header" | "footer", got: unknown, allowed: readonly string[]) {
252
+ super(
253
+ `Invalid ${kind}Variant ${JSON.stringify(got)} — must be one of: ${allowed.join(", ")}. ` +
254
+ `${kind === "header" ? "headerVariant" : "footerVariant"} is assigned explicitly per page (Layout.astro props); it is never inferred from the URL, a CSS class, or the page title.`,
255
+ );
256
+ this.name = "InvalidChromeVariantError";
257
+ }
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Header / footer — independently-evolvable sub-shapes.
262
+ // ---------------------------------------------------------------------------
263
+
264
+ export interface ChromeHeader {
265
+ variant: HeaderVariant;
266
+ brand: ChromeBrand;
267
+ /** Primary nav — flat links and/or mega panels (ChromeNavItem.columns). */
268
+ nav: ChromeNavItem[];
269
+ search?: ChromeSearch;
270
+ utilityNav?: ChromeUtilityNav;
271
+ announcement?: ChromeAnnouncement;
272
+ /** Header-actions member-price cue (in addition to any utility-nav cue). */
273
+ memberCue?: ChromeActionLink;
274
+ accountHref?: string;
275
+ cartHref?: string;
276
+ /** Marketing conversion CTAs (non-commerce tenants), shown in the header's
277
+ * actions cluster in place of the commerce search/saved controls. */
278
+ ctas?: ChromeCta[];
279
+ }
280
+
281
+ export interface ChromeFooter {
282
+ variant: FooterVariant;
283
+ brand?: ChromeFooterBrand;
284
+ columns: FooterColumn[];
285
+ newsletter?: FooterNewsletter;
286
+ trustMarks?: ChromeTrustMark[];
287
+ /** Accepted-payment marks (text tokens, e.g. "VISA"). */
288
+ payments?: string[];
289
+ social?: SocialLink[];
290
+ /** The bold compliance/nicotine warning block. */
291
+ warning?: string;
292
+ /** Bottom bar: copyright + a legal note line. */
293
+ bottom?: { copyright?: string; note?: string };
294
+ }
295
+
296
+ /**
297
+ * The full tenant chrome config (content only) authored as `chrome.json`.
298
+ * CONTENT ONLY — every color/space/type comes from theme tokens via the
299
+ * platform-shipped stylesheet; this config carries no styling.
300
+ */
301
+ export interface ChromeConfig {
302
+ /** <html lang> (defaults to "en"). */
303
+ lang?: string;
304
+ head?: ChromeHead;
305
+ /** Skip-link target/label overrides (defaults to "#main" / "Skip to main content"). */
306
+ skipLink?: { href?: string; label?: string };
307
+ /** FDA/compliance warning strip at the very top (e.g. the nicotine line). */
308
+ warningBar?: string;
309
+ /**
310
+ * OPT-IN: also link the shared commerce-MARKETING stylesheet
311
+ * (/shared/commerce-marketing.css) into this tenant's wrapper — the
312
+ * raw-page BODY layer (page-hero / prose / callouts / value & tier cards /
313
+ * contact form / FAQ / CTA band) its content fragments repeat. Linked AFTER
314
+ * the chrome sheet so its page-level variants win.
315
+ */
316
+ sharedMarketingCss?: boolean;
317
+ header: ChromeHeader;
318
+ footer: ChromeFooter;
319
+ ageGate?: ChromeAgeGate;
320
+ /** The compliance strip below the page body (e.g. the nicotine warning). */
321
+ complianceStrip?: string;
322
+ /** Default legal/safety disclaimer HTML (fallback for unknown categories). */
323
+ disclaimer?: string;
324
+ /** Per-product-category disclaimer HTML, keyed by product_type (or
325
+ * compliance.product_category) — the source store renders a different legal
326
+ * block per type (sub-ohm on salts, battery on devices). */
327
+ disclaimers?: Record<string, string>;
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Validation — fail-closed schema validation for a tenant's `chrome.json`.
332
+ // ---------------------------------------------------------------------------
333
+
334
+ export interface ChromeConfigValidationResult {
335
+ ok: boolean;
336
+ config: ChromeConfig | null;
337
+ /** Actionable, path-prefixed messages, e.g. "header.nav[2].id: required, non-empty string". */
338
+ errors: string[];
339
+ }
340
+
341
+ function isObject(v: unknown): v is Record<string, unknown> {
342
+ return typeof v === "object" && v !== null && !Array.isArray(v);
343
+ }
344
+
345
+ function isNonEmptyString(v: unknown): v is string {
346
+ return typeof v === "string" && v.trim().length > 0;
347
+ }
348
+
349
+ function checkOptionalString(v: unknown, path: string, errors: string[]): void {
350
+ if (v !== undefined && typeof v !== "string") errors.push(`${path}: must be a string`);
351
+ }
352
+
353
+ function checkOptionalBoolean(v: unknown, path: string, errors: string[]): void {
354
+ if (v !== undefined && typeof v !== "boolean") errors.push(`${path}: must be a boolean`);
355
+ }
356
+
357
+ /** `ChromeLink` — the base actionable-item shape: a stable `id` + `label` + `href`. */
358
+ function validateLink(v: unknown, path: string, errors: string[]): v is ChromeLink {
359
+ if (!isObject(v)) {
360
+ errors.push(`${path}: must be an object`);
361
+ return false;
362
+ }
363
+ const before = errors.length;
364
+ if (!isNonEmptyString(v.id)) {
365
+ errors.push(
366
+ `${path}.id: required, non-empty string — a stable identifier independent of ` +
367
+ `\`label\` copy (the governed-action-registry substitute; every nav/link/CTA needs one)`,
368
+ );
369
+ }
370
+ if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
371
+ if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
372
+ checkOptionalBoolean(v.external, `${path}.external`, errors);
373
+ checkOptionalString(v.badge, `${path}.badge`, errors);
374
+ return errors.length === before;
375
+ }
376
+
377
+ /** An action-link (member-price cue etc.): `id` + `label` + `href`, no badge/external. */
378
+ function validateActionLink(v: unknown, path: string, errors: string[]): boolean {
379
+ if (!isObject(v)) {
380
+ errors.push(`${path}: must be an object`);
381
+ return false;
382
+ }
383
+ const before = errors.length;
384
+ if (!isNonEmptyString(v.id)) errors.push(`${path}.id: required, non-empty string`);
385
+ if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
386
+ if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
387
+ return errors.length === before;
388
+ }
389
+
390
+ function validateMegaColumn(v: unknown, path: string, errors: string[]): v is MegaColumn {
391
+ if (!isObject(v)) {
392
+ errors.push(`${path}: must be an object`);
393
+ return false;
394
+ }
395
+ const before = errors.length;
396
+ checkOptionalString(v.title, `${path}.title`, errors);
397
+ if (!Array.isArray(v.links)) {
398
+ errors.push(`${path}.links: required array`);
399
+ } else {
400
+ v.links.forEach((l, i) => validateLink(l, `${path}.links[${i}]`, errors));
401
+ }
402
+ return errors.length === before;
403
+ }
404
+
405
+ function validateNavItem(v: unknown, path: string, errors: string[]): v is ChromeNavItem {
406
+ if (!isObject(v)) {
407
+ errors.push(`${path}: must be an object`);
408
+ return false;
409
+ }
410
+ const before = errors.length;
411
+ if (!isNonEmptyString(v.id)) {
412
+ errors.push(`${path}.id: required, non-empty string — a stable identifier independent of \`label\` copy`);
413
+ }
414
+ if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
415
+ if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
416
+ checkOptionalString(v.badge, `${path}.badge`, errors);
417
+ checkOptionalBoolean(v.flag, `${path}.flag`, errors);
418
+ if (v.children !== undefined) {
419
+ if (!Array.isArray(v.children)) errors.push(`${path}.children: must be an array`);
420
+ else v.children.forEach((c, i) => validateLink(c, `${path}.children[${i}]`, errors));
421
+ }
422
+ if (v.columns !== undefined) {
423
+ if (!Array.isArray(v.columns)) errors.push(`${path}.columns: must be an array`);
424
+ else v.columns.forEach((c, i) => validateMegaColumn(c, `${path}.columns[${i}]`, errors));
425
+ }
426
+ return errors.length === before;
427
+ }
428
+
429
+ function validateCta(v: unknown, path: string, errors: string[]): v is ChromeCta {
430
+ if (!isObject(v)) {
431
+ errors.push(`${path}: must be an object`);
432
+ return false;
433
+ }
434
+ const before = errors.length;
435
+ if (!isNonEmptyString(v.id)) errors.push(`${path}.id: required, non-empty string`);
436
+ if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
437
+ if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
438
+ if (v.variant !== undefined && !["primary", "secondary", "ghost"].includes(v.variant as string)) {
439
+ errors.push(`${path}.variant: must be one of primary, secondary, ghost`);
440
+ }
441
+ return errors.length === before;
442
+ }
443
+
444
+ function validateSocialLink(v: unknown, path: string, errors: string[]): v is SocialLink {
445
+ if (!isObject(v)) {
446
+ errors.push(`${path}: must be an object`);
447
+ return false;
448
+ }
449
+ const before = errors.length;
450
+ if (!isNonEmptyString(v.label)) errors.push(`${path}.label: required, non-empty string`);
451
+ if (!isNonEmptyString(v.href)) errors.push(`${path}.href: required, non-empty string`);
452
+ checkOptionalString(v.iconPath, `${path}.iconPath`, errors);
453
+ return errors.length === before;
454
+ }
455
+
456
+ function validateFooterColumn(v: unknown, path: string, errors: string[]): v is FooterColumn {
457
+ if (!isObject(v)) {
458
+ errors.push(`${path}: must be an object`);
459
+ return false;
460
+ }
461
+ const before = errors.length;
462
+ if (!isNonEmptyString(v.title)) errors.push(`${path}.title: required, non-empty string`);
463
+ if (!Array.isArray(v.links)) {
464
+ errors.push(`${path}.links: required array`);
465
+ } else {
466
+ v.links.forEach((l, i) => validateLink(l, `${path}.links[${i}]`, errors));
467
+ }
468
+ return errors.length === before;
469
+ }
470
+
471
+ function validateAnnouncement(v: unknown, path: string, errors: string[]): v is ChromeAnnouncement {
472
+ if (!isObject(v)) {
473
+ errors.push(`${path}: must be an object`);
474
+ return false;
475
+ }
476
+ const before = errors.length;
477
+ if (!["marquee", "bar", "solid"].includes(v.variant as string)) {
478
+ errors.push(`${path}.variant: must be one of marquee, bar, solid`);
479
+ }
480
+ if (!Array.isArray(v.messages) || !v.messages.every((m) => typeof m === "string")) {
481
+ errors.push(`${path}.messages: required array of strings`);
482
+ }
483
+ return errors.length === before;
484
+ }
485
+
486
+ function validateAgeGate(v: unknown, path: string, errors: string[]): v is ChromeAgeGate {
487
+ if (!isObject(v)) {
488
+ errors.push(`${path}: must be an object`);
489
+ return false;
490
+ }
491
+ const before = errors.length;
492
+ if (typeof v.minAge !== "number" || !Number.isFinite(v.minAge)) {
493
+ errors.push(`${path}.minAge: required number`);
494
+ }
495
+ return errors.length === before;
496
+ }
497
+
498
+ function validateHeader(v: unknown, path: string, errors: string[]): v is ChromeHeader {
499
+ if (!isObject(v)) {
500
+ errors.push(`${path}: required object`);
501
+ return false;
502
+ }
503
+ const before = errors.length;
504
+ if (!isHeaderVariant(v.variant)) {
505
+ errors.push(`${path}.variant: required, must be one of ${HEADER_VARIANTS.join(", ")} (got ${JSON.stringify(v.variant)})`);
506
+ }
507
+ if (!isObject(v.brand) || !isNonEmptyString((v.brand as Record<string, unknown>).label)) {
508
+ errors.push(`${path}.brand: required object with a non-empty \`label\``);
509
+ }
510
+ if (!Array.isArray(v.nav)) {
511
+ errors.push(`${path}.nav: required array`);
512
+ } else {
513
+ v.nav.forEach((n, i) => validateNavItem(n, `${path}.nav[${i}]`, errors));
514
+ }
515
+ if (v.search !== undefined && !isObject(v.search)) errors.push(`${path}.search: must be an object`);
516
+ if (v.utilityNav !== undefined) {
517
+ const u = v.utilityNav;
518
+ if (!isObject(u)) {
519
+ errors.push(`${path}.utilityNav: must be an object`);
520
+ } else {
521
+ (u.links as unknown[] | undefined)?.forEach((l, i) => validateLink(l, `${path}.utilityNav.links[${i}]`, errors));
522
+ (u.social as unknown[] | undefined)?.forEach((s, i) => validateSocialLink(s, `${path}.utilityNav.social[${i}]`, errors));
523
+ if (u.memberCue !== undefined) validateActionLink(u.memberCue, `${path}.utilityNav.memberCue`, errors);
524
+ }
525
+ }
526
+ if (v.announcement !== undefined) validateAnnouncement(v.announcement, `${path}.announcement`, errors);
527
+ if (v.memberCue !== undefined) validateActionLink(v.memberCue, `${path}.memberCue`, errors);
528
+ checkOptionalString(v.accountHref, `${path}.accountHref`, errors);
529
+ checkOptionalString(v.cartHref, `${path}.cartHref`, errors);
530
+ if (v.ctas !== undefined) {
531
+ if (!Array.isArray(v.ctas)) errors.push(`${path}.ctas: must be an array`);
532
+ else v.ctas.forEach((c, i) => validateCta(c, `${path}.ctas[${i}]`, errors));
533
+ }
534
+ return errors.length === before;
535
+ }
536
+
537
+ function validateFooter(v: unknown, path: string, errors: string[]): v is ChromeFooter {
538
+ if (!isObject(v)) {
539
+ errors.push(`${path}: required object`);
540
+ return false;
541
+ }
542
+ const before = errors.length;
543
+ if (!isFooterVariant(v.variant)) {
544
+ errors.push(`${path}.variant: required, must be one of ${FOOTER_VARIANTS.join(", ")} (got ${JSON.stringify(v.variant)})`);
545
+ }
546
+ if (!Array.isArray(v.columns)) {
547
+ errors.push(`${path}.columns: required array`);
548
+ } else {
549
+ v.columns.forEach((c, i) => validateFooterColumn(c, `${path}.columns[${i}]`, errors));
550
+ }
551
+ if (v.social !== undefined) {
552
+ if (!Array.isArray(v.social)) errors.push(`${path}.social: must be an array`);
553
+ else v.social.forEach((s, i) => validateSocialLink(s, `${path}.social[${i}]`, errors));
554
+ }
555
+ if (v.payments !== undefined && (!Array.isArray(v.payments) || !v.payments.every((p) => typeof p === "string"))) {
556
+ errors.push(`${path}.payments: must be an array of strings`);
557
+ }
558
+ checkOptionalString(v.warning, `${path}.warning`, errors);
559
+ if (v.brand !== undefined && !isObject(v.brand)) errors.push(`${path}.brand: must be an object`);
560
+ if (v.newsletter !== undefined && !isObject(v.newsletter)) errors.push(`${path}.newsletter: must be an object`);
561
+ if (v.bottom !== undefined && !isObject(v.bottom)) errors.push(`${path}.bottom: must be an object`);
562
+ return errors.length === before;
563
+ }
564
+
565
+ /**
566
+ * Validate `raw` (parsed JSON) as a `ChromeConfig`. Collects every violation
567
+ * (not just the first) so a caller gets one actionable report. Trusts the
568
+ * authored shape beyond these checks — the renderers are defensive (escape
569
+ * values, guard every optional section), so a missing optional degrades
570
+ * rather than breaks.
571
+ */
572
+ export function validateChromeConfig(raw: unknown): ChromeConfigValidationResult {
573
+ const errors: string[] = [];
574
+ if (!isObject(raw)) {
575
+ return { ok: false, config: null, errors: ["chrome.json: must be a JSON object"] };
576
+ }
577
+ checkOptionalString(raw.lang, "lang", errors);
578
+ checkOptionalBoolean(raw.sharedMarketingCss, "sharedMarketingCss", errors);
579
+ validateHeader(raw.header, "header", errors);
580
+ validateFooter(raw.footer, "footer", errors);
581
+ if (raw.ageGate !== undefined) validateAgeGate(raw.ageGate, "ageGate", errors);
582
+ checkOptionalString(raw.complianceStrip, "complianceStrip", errors);
583
+ checkOptionalString(raw.disclaimer, "disclaimer", errors);
584
+ if (raw.disclaimers !== undefined && !isObject(raw.disclaimers)) {
585
+ errors.push("disclaimers: must be an object keyed by product_type");
586
+ }
587
+ if (raw.head !== undefined && !isObject(raw.head)) errors.push("head: must be an object");
588
+ if (raw.skipLink !== undefined && !isObject(raw.skipLink)) errors.push("skipLink: must be an object");
589
+
590
+ if (errors.length > 0) return { ok: false, config: null, errors };
591
+ return { ok: true, config: raw as unknown as ChromeConfig, errors: [] };
592
+ }
593
+
594
+ /**
595
+ * Parse `raw` into a `ChromeConfig`, or null if it fails validation. Drop-in
596
+ * replacement for the retired `parseChromeConfig` opt-in gate: callers that
597
+ * only need "is this adopted" keep their existing null-means-not-adopted
598
+ * contract; callers that want the actionable report call
599
+ * {@link validateChromeConfig} directly.
600
+ */
601
+ export function parseChromeConfig(raw: unknown): ChromeConfig | null {
602
+ return validateChromeConfig(raw).config;
603
+ }
604
+
605
+ // ---------------------------------------------------------------------------
606
+ // Page-assignment manifest — per-page header/footer variant overrides for the
607
+ // raw-HTML rendering path (`content/<tenant>/pages-html/**.html`).
608
+ //
609
+ // Layout.astro's headerVariant/footerVariant props only reach pages rendered as
610
+ // Astro components. A raw-HTML page renders via ContentProvider.getChromeHtml(),
611
+ // which has no per-page variant argument and always used the tenant's single
612
+ // chrome.json default. `content/<tenant>/chrome-assignments.json` is the
613
+ // raw-HTML equivalent of that prop.
614
+ // ---------------------------------------------------------------------------
615
+
616
+ /** One page's variant override. At least one field must be set — an entry that
617
+ * overrides neither overrides nothing. */
618
+ export interface ChromeAssignmentEntry {
619
+ headerVariant?: HeaderVariant;
620
+ footerVariant?: FooterVariant;
621
+ }
622
+
623
+ /**
624
+ * The manifest: a map keyed by page path, so a second, conflicting assignment
625
+ * for the same page is structurally unrepresentable rather than
626
+ * representable-and-then-rejected.
627
+ *
628
+ * Keys are FULL page paths with a leading slash ("/", "/contact/contact-sales")
629
+ * — the exact string form `getChromeHtml(currentPath)` already receives
630
+ * (`` `/${slug}` `` from [...slug].astro, "/" from index.astro), so the resolver
631
+ * does no format translation on the hot path.
632
+ */
633
+ export interface ChromeAssignments {
634
+ assignments: Record<string, ChromeAssignmentEntry>;
635
+ }
636
+
637
+ export interface ChromeAssignmentsValidationResult {
638
+ ok: boolean;
639
+ assignments: ChromeAssignments | null;
640
+ /** Structural violations — fail closed. */
641
+ errors: string[];
642
+ /** Non-fatal notes, e.g. an assignment naming a page that doesn't exist yet. */
643
+ warnings: string[];
644
+ }
645
+
646
+ export interface ValidateChromeAssignmentsOptions {
647
+ /**
648
+ * Every page path that currently exists, in the same leading-slash form as
649
+ * the keys. When given, a key naming a page outside it is a WARNING, never an
650
+ * error: the assignment and the page it targets are naturally authored
651
+ * together, so erroring would make the manifest unauthorable until the page
652
+ * lands. Omit it entirely (rather than passing a guess) where the page set
653
+ * isn't known — the check is then skipped silently.
654
+ */
655
+ knownPagePaths?: readonly string[];
656
+ }
657
+
658
+ const CHROME_ASSIGNMENT_ENTRY_KEYS = ["headerVariant", "footerVariant"] as const;
659
+
660
+ /** Trailing-slash-insensitive page-path form. "/" is preserved as-is. */
661
+ export function normalizeChromePagePath(path: string): string {
662
+ return path.length > 1 ? path.replace(/\/+$/, "") : path;
663
+ }
664
+
665
+ function validateAssignmentEntry(v: unknown, path: string, errors: string[]): void {
666
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
667
+ errors.push(`${path}: must be an object with headerVariant and/or footerVariant`);
668
+ return;
669
+ }
670
+ const entry = v as Record<string, unknown>;
671
+ for (const key of Object.keys(entry)) {
672
+ if (!(CHROME_ASSIGNMENT_ENTRY_KEYS as readonly string[]).includes(key)) {
673
+ errors.push(
674
+ `${path}.${key}: unknown field — only ${CHROME_ASSIGNMENT_ENTRY_KEYS.join(" and ")} may be set`,
675
+ );
676
+ }
677
+ }
678
+ if (entry.headerVariant !== undefined && !isHeaderVariant(entry.headerVariant)) {
679
+ errors.push(
680
+ `${path}.headerVariant: must be one of ${HEADER_VARIANTS.join(", ")} (got ${JSON.stringify(entry.headerVariant)})`,
681
+ );
682
+ }
683
+ if (entry.footerVariant !== undefined && !isFooterVariant(entry.footerVariant)) {
684
+ errors.push(
685
+ `${path}.footerVariant: must be one of ${FOOTER_VARIANTS.join(", ")} (got ${JSON.stringify(entry.footerVariant)})`,
686
+ );
687
+ }
688
+ if (entry.headerVariant === undefined && entry.footerVariant === undefined) {
689
+ errors.push(`${path}: must set headerVariant and/or footerVariant — an entry that overrides neither overrides nothing`);
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Validate `raw` as a page-assignment manifest. Collects every structural
695
+ * violation rather than stopping at the first, like {@link validateChromeConfig}.
696
+ */
697
+ export function validateChromeAssignments(
698
+ raw: unknown,
699
+ options: ValidateChromeAssignmentsOptions = {},
700
+ ): ChromeAssignmentsValidationResult {
701
+ const errors: string[] = [];
702
+ const warnings: string[] = [];
703
+
704
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
705
+ return {
706
+ ok: false,
707
+ assignments: null,
708
+ errors: ['chrome-assignments.json: must be a JSON object of the form { "assignments": { "/page": { … } } }'],
709
+ warnings,
710
+ };
711
+ }
712
+ const doc = raw as Record<string, unknown>;
713
+ for (const key of Object.keys(doc)) {
714
+ if (key !== "assignments") {
715
+ errors.push(`${key}: unknown top-level field — the only field is "assignments"`);
716
+ }
717
+ }
718
+ const map = doc.assignments;
719
+ if (typeof map !== "object" || map === null || Array.isArray(map)) {
720
+ errors.push('assignments: required — an object keyed by page path (e.g. { "/contact/contact-sales": { "headerVariant": "minimal" } })');
721
+ return { ok: false, assignments: null, errors, warnings };
722
+ }
723
+
724
+ const entries = map as Record<string, unknown>;
725
+ for (const key of Object.keys(entries)) {
726
+ const at = `assignments[${JSON.stringify(key)}]`;
727
+ if (!key.startsWith("/")) {
728
+ errors.push(`${at}: page path must start with "/" (e.g. "/${key}")`);
729
+ } else if (key.length > 1 && key.endsWith("/")) {
730
+ errors.push(`${at}: page path must not end with "/" — author it as "${normalizeChromePagePath(key)}"`);
731
+ }
732
+ validateAssignmentEntry(entries[key], at, errors);
733
+ }
734
+ if (errors.length > 0) return { ok: false, assignments: null, errors, warnings };
735
+
736
+ if (options.knownPagePaths) {
737
+ const known = new Set(options.knownPagePaths.map(normalizeChromePagePath));
738
+ for (const key of Object.keys(entries)) {
739
+ if (!known.has(normalizeChromePagePath(key))) {
740
+ warnings.push(
741
+ `assignments[${JSON.stringify(key)}]: no page exists at this path yet — the assignment applies once the page lands`,
742
+ );
743
+ }
744
+ }
745
+ }
746
+
747
+ return {
748
+ ok: true,
749
+ assignments: { assignments: entries as Record<string, ChromeAssignmentEntry> },
750
+ errors,
751
+ warnings,
752
+ };
753
+ }
754
+
755
+ /**
756
+ * Parse `raw` into `ChromeAssignments`, or null if absent or invalid — the same
757
+ * null-means-not-adopted contract as {@link parseChromeConfig}.
758
+ */
759
+ export function parseChromeAssignments(raw: unknown): ChromeAssignments | null {
760
+ if (raw === null || raw === undefined) return null;
761
+ return validateChromeAssignments(raw).assignments;
762
+ }
763
+
764
+ /**
765
+ * The entry assigned to `currentPath`, if any. Exact match only (after
766
+ * trailing-slash normalization) — deliberately no prefix or glob matching:
767
+ * real tenant trees nest pages under a shared prefix ("/contact",
768
+ * "/contact/contact-sales", "/contact/startup-apply"), so a prefix rule would
769
+ * sweep in pages nobody assigned.
770
+ */
771
+ export function resolveChromeAssignment(
772
+ assignments: ChromeAssignments | null | undefined,
773
+ currentPath: string | undefined,
774
+ ): ChromeAssignmentEntry | undefined {
775
+ if (!assignments || currentPath === undefined) return undefined;
776
+ return assignments.assignments[normalizeChromePagePath(currentPath)];
777
+ }