@tokenoftrust/storefront-runner 1.4.1 → 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 (474) hide show
  1. package/apps/storefront/astro.config.mjs +17 -1
  2. package/apps/storefront/env.d.ts +54 -5
  3. package/apps/storefront/integrations/materialize-guard.mjs +54 -0
  4. package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
  5. package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
  6. package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
  7. package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
  8. package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
  9. package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
  10. package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
  11. package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
  12. package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
  13. package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
  14. package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
  15. package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
  16. package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
  17. package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
  18. package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
  19. package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
  20. package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
  21. package/apps/storefront/migrations-apps/meta/_journal.json +56 -0
  22. package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
  23. package/apps/storefront/public/js/dashboard-team.js +38 -2
  24. package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
  25. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
  26. package/apps/storefront/public/shared/commerce-chrome.css +20 -2
  27. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
  28. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
  29. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
  30. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
  31. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
  32. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
  33. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
  34. package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
  35. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
  36. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
  37. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
  38. package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
  39. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
  40. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
  41. package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
  42. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
  43. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
  44. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
  45. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
  46. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
  47. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
  48. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
  49. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
  50. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
  51. package/apps/storefront/src/components/Img.astro +1 -1
  52. package/apps/storefront/src/components/Seo.astro +65 -1
  53. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
  54. package/apps/storefront/src/components/admin/AdminBlogTab.astro +1435 -0
  55. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
  56. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
  57. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1980 -2564
  58. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  59. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
  60. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
  61. package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
  62. package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
  63. package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
  64. package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
  65. package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
  66. package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
  67. package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
  68. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
  69. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
  70. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
  71. package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
  72. package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
  73. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
  74. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
  75. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
  76. package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
  77. package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
  78. package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
  79. package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
  80. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
  81. package/apps/storefront/src/components/membership/TierCards.astro +2 -2
  82. package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
  83. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
  84. package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
  85. package/apps/storefront/src/config/devTenantSeed.ts +9 -9
  86. package/apps/storefront/src/config/resolver.ts +4 -4
  87. package/apps/storefront/src/layouts/Layout.astro +146 -21
  88. package/apps/storefront/src/lib/activity/alerts.ts +17 -19
  89. package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
  90. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +89 -9
  91. package/apps/storefront/src/lib/activity/deployVersion.ts +20 -27
  92. package/apps/storefront/src/lib/activity/ingest.ts +8 -8
  93. package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
  94. package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
  95. package/apps/storefront/src/lib/activity/query.ts +9 -9
  96. package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
  97. package/apps/storefront/src/lib/activity/store.ts +4 -6
  98. package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
  99. package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
  100. package/apps/storefront/src/lib/admin/adminShell.ts +292 -0
  101. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
  102. package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
  103. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
  104. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
  105. package/apps/storefront/src/lib/admin/envelope.ts +1 -2
  106. package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
  107. package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
  108. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
  109. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
  110. package/apps/storefront/src/lib/apps/adminService.ts +14 -4
  111. package/apps/storefront/src/lib/apps/adminSession.ts +54 -45
  112. package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
  113. package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
  114. package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
  115. package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
  116. package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
  117. package/apps/storefront/src/lib/apps/credentials.ts +2 -2
  118. package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
  119. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
  120. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
  121. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
  122. package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
  123. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
  124. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
  125. package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
  126. package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
  127. package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
  128. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
  129. package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
  130. package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
  131. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
  132. package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
  133. package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
  134. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +9 -1
  135. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
  136. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +7 -7
  137. package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
  138. package/apps/storefront/src/lib/apps/registryService.ts +270 -16
  139. package/apps/storefront/src/lib/apps/scopes.ts +2 -2
  140. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
  141. package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
  142. package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
  143. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
  144. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
  145. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
  146. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
  147. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
  148. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
  149. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
  150. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
  151. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
  152. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
  153. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
  154. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
  155. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
  156. package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
  157. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
  158. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
  159. package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
  160. package/apps/storefront/src/lib/auth/adminEntry.ts +11 -11
  161. package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
  162. package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
  163. package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
  164. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
  165. package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
  166. package/apps/storefront/src/lib/auth/session.ts +7 -8
  167. package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
  168. package/apps/storefront/src/lib/basePath.ts +9 -2
  169. package/apps/storefront/src/lib/blog/access.ts +29 -0
  170. package/apps/storefront/src/lib/blog/collection.ts +400 -0
  171. package/apps/storefront/src/lib/blog/markdown.ts +174 -0
  172. package/apps/storefront/src/lib/blog/provider.ts +62 -3
  173. package/apps/storefront/src/lib/blog/reactions.ts +87 -0
  174. package/apps/storefront/src/lib/blog/rss.ts +92 -0
  175. package/apps/storefront/src/lib/blog/teaser.ts +83 -0
  176. package/apps/storefront/src/lib/blog/types.ts +34 -0
  177. package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
  178. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
  179. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
  180. package/apps/storefront/src/lib/chrome/model.ts +48 -253
  181. package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
  182. package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
  183. package/apps/storefront/src/lib/chrome/variant.ts +39 -0
  184. package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
  185. package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
  186. package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
  187. package/apps/storefront/src/lib/content/index.ts +5 -5
  188. package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
  189. package/apps/storefront/src/lib/content-edit/route.ts +1 -1
  190. package/apps/storefront/src/lib/d1/catalog.ts +14 -2
  191. package/apps/storefront/src/lib/d1/schema-apps.ts +372 -17
  192. package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
  193. package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
  194. package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
  195. package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
  196. package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
  197. package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
  198. package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
  199. package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
  200. package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
  201. package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
  202. package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
  203. package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
  204. package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
  205. package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
  206. package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
  207. package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
  208. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
  209. package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
  210. package/apps/storefront/src/lib/edgeCache.ts +34 -5
  211. package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
  212. package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
  213. package/apps/storefront/src/lib/i18n.ts +25 -0
  214. package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
  215. package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
  216. package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
  217. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
  218. package/apps/storefront/src/lib/jsonld.ts +118 -0
  219. package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
  220. package/apps/storefront/src/lib/membership/model.ts +1 -2
  221. package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
  222. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
  223. package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
  224. package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
  225. package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
  226. package/apps/storefront/src/lib/newsletter/send.ts +265 -0
  227. package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
  228. package/apps/storefront/src/lib/previewReload.ts +57 -1
  229. package/apps/storefront/src/lib/publish/adminPublishTab.ts +4625 -0
  230. package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
  231. package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
  232. package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
  233. package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
  234. package/apps/storefront/src/lib/publish/domainState.ts +22 -13
  235. package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
  236. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
  237. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
  238. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
  239. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
  240. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
  241. package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
  242. package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
  243. package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
  244. package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
  245. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
  246. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
  247. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
  248. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
  249. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
  250. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
  251. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
  252. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
  253. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
  254. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
  255. package/apps/storefront/src/lib/publish/shipWorkspace.ts +121 -51
  256. package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
  257. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
  258. package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
  259. package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
  260. package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
  261. package/apps/storefront/src/lib/rawChrome.ts +125 -47
  262. package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
  263. package/apps/storefront/src/lib/search/index.ts +2 -2
  264. package/apps/storefront/src/lib/seo/alternates.ts +42 -0
  265. package/apps/storefront/src/lib/seo/meta.ts +65 -0
  266. package/apps/storefront/src/lib/social/golive.ts +212 -0
  267. package/apps/storefront/src/lib/social/notify.ts +32 -0
  268. package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
  269. package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
  270. package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
  271. package/apps/storefront/src/lib/subscription/model.ts +4 -4
  272. package/apps/storefront/src/lib/the-build/provider.ts +32 -0
  273. package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
  274. package/apps/storefront/src/lib/the-build/toc.ts +95 -0
  275. package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
  276. package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
  277. package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
  278. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
  279. package/apps/storefront/src/middleware/index.ts +385 -52
  280. package/apps/storefront/src/pages/[...slug].astro +9 -4
  281. package/apps/storefront/src/pages/admin/review-release.astro +265 -0
  282. package/apps/storefront/src/pages/admin/select.astro +96 -0
  283. package/apps/storefront/src/pages/admin.astro +25 -1313
  284. package/apps/storefront/src/pages/api/activity.ts +8 -8
  285. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
  286. package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
  287. package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
  288. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
  289. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
  290. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
  291. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
  292. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
  293. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
  294. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
  295. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
  296. package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
  297. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
  298. package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
  299. package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
  300. package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
  301. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
  302. package/apps/storefront/src/pages/api/rum/vitals.ts +20 -2
  303. package/apps/storefront/src/pages/blog/[slug].astro +99 -19
  304. package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
  305. package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
  306. package/apps/storefront/src/pages/blog/index.astro +4 -2
  307. package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
  308. package/apps/storefront/src/pages/cockpit.astro +77 -1353
  309. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
  310. package/apps/storefront/src/pages/index.astro +10 -5
  311. package/apps/storefront/src/pages/llms.txt.ts +1 -1
  312. package/apps/storefront/src/pages/products/[handle].astro +7 -6
  313. package/apps/storefront/src/pages/search.astro +1 -1
  314. package/apps/storefront/src/pages/sitemap.xml.ts +21 -1
  315. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
  316. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
  317. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
  318. package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
  319. package/apps/storefront/src/pages/the-build/index.astro +146 -0
  320. package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
  321. package/apps/storefront/src/styles/admin.css +1106 -0
  322. package/apps/storefront/tsconfig.json +6 -1
  323. package/package.json +1 -1
  324. package/packages/public-runtime/package.json +3 -1
  325. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
  326. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
  327. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
  328. package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
  329. package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
  330. package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
  331. package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
  332. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
  333. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
  334. package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
  335. package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
  336. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
  337. package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
  338. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
  339. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
  340. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
  341. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
  342. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
  343. package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
  344. package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
  345. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
  346. package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
  347. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
  348. package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
  349. package/packages/public-runtime/src/activity/catalog.ts +89 -33
  350. package/packages/public-runtime/src/activity/event.ts +6 -6
  351. package/packages/public-runtime/src/activity/index.ts +2 -2
  352. package/packages/public-runtime/src/activity/redaction.ts +4 -4
  353. package/packages/public-runtime/src/candidate-index.ts +42 -8
  354. package/packages/public-runtime/src/catalog-d1.ts +1 -1
  355. package/packages/public-runtime/src/checkout-orders.ts +2 -2
  356. package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
  357. package/packages/public-runtime/src/checkout-style.ts +7 -7
  358. package/packages/public-runtime/src/checkout.ts +3 -3
  359. package/packages/public-runtime/src/chrome.ts +777 -0
  360. package/packages/public-runtime/src/compliance-obligations.ts +310 -0
  361. package/packages/public-runtime/src/csp.ts +99 -0
  362. package/packages/public-runtime/src/customization-preview.ts +25 -2
  363. package/packages/public-runtime/src/customization-runtime.ts +156 -3
  364. package/packages/public-runtime/src/customization-versioning.ts +10 -0
  365. package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
  366. package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
  367. package/packages/public-runtime/src/embed-catalog.mjs +40 -0
  368. package/packages/public-runtime/src/evidence.ts +288 -0
  369. package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
  370. package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
  371. package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
  372. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
  373. package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
  374. package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
  375. package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
  376. package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
  377. package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
  378. package/packages/public-runtime/src/index.ts +17 -1
  379. package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
  380. package/packages/public-runtime/src/integration/assurance.ts +228 -0
  381. package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
  382. package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
  383. package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
  384. package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
  385. package/packages/public-runtime/src/integration/consent-config.ts +260 -0
  386. package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
  387. package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
  388. package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
  389. package/packages/public-runtime/src/integration/csp.ts +105 -0
  390. package/packages/public-runtime/src/integration/digest.ts +43 -0
  391. package/packages/public-runtime/src/integration/environment.ts +228 -0
  392. package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
  393. package/packages/public-runtime/src/integration/events.ts +701 -0
  394. package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
  395. package/packages/public-runtime/src/integration/index.ts +90 -0
  396. package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
  397. package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
  398. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
  399. package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
  400. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
  401. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
  402. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
  403. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
  404. package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
  405. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
  406. package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
  407. package/packages/public-runtime/src/integration/manifest.ts +470 -0
  408. package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
  409. package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
  410. package/packages/public-runtime/src/integration/observability.ts +451 -0
  411. package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
  412. package/packages/public-runtime/src/integration/phases.ts +182 -0
  413. package/packages/public-runtime/src/integration/plan.ts +112 -0
  414. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
  415. package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
  416. package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
  417. package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
  418. package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
  419. package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
  420. package/packages/public-runtime/src/integration/render-phases.ts +267 -0
  421. package/packages/public-runtime/src/integration/resolve.ts +465 -0
  422. package/packages/public-runtime/src/integration/result.ts +239 -0
  423. package/packages/public-runtime/src/integration/secrets.ts +92 -0
  424. package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
  425. package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
  426. package/packages/public-runtime/src/integration/version.ts +92 -0
  427. package/packages/public-runtime/src/integration-capabilities.ts +520 -0
  428. package/packages/public-runtime/src/membership.ts +2 -2
  429. package/packages/public-runtime/src/raw-html-policy.ts +2 -4
  430. package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
  431. package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
  432. package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
  433. package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
  434. package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
  435. package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
  436. package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
  437. package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
  438. package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
  439. package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
  440. package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
  441. package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
  442. package/packages/public-runtime/src/static-bundle.ts +357 -0
  443. package/packages/public-runtime/src/tenant-assets.ts +143 -4
  444. package/packages/public-runtime/src/tenant.ts +81 -17
  445. package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
  446. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
  447. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
  448. package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
  449. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
  450. package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
  451. package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
  452. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
  453. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
  454. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
  455. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
  456. package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
  457. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
  458. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
  459. package/scripts/build/copy-tenant-assets.mjs +14 -4
  460. package/scripts/dev/ai-edit.mjs +1 -1
  461. package/scripts/dev/checkout-watch.mjs +2 -2
  462. package/scripts/dev/file-browser.mjs +4 -6
  463. package/scripts/dev/file-writer.mjs +3 -3
  464. package/scripts/dev/git-status.mjs +2 -2
  465. package/scripts/dev/integration-guardrails.mjs +498 -0
  466. package/scripts/dev/locate-handler.mjs +3 -3
  467. package/scripts/dev/port-check.mjs +3 -3
  468. package/scripts/dev/publish.mjs +5 -9
  469. package/scripts/dev/safe-path.mjs +2 -2
  470. package/scripts/dev/shot.mjs +1 -1
  471. package/scripts/dev/tenant-source-root.mjs +1 -1
  472. package/scripts/dev/unified-diff.mjs +8 -3
  473. package/scripts/tot-dev.mjs +8 -11
  474. package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Pure formatting for the admin Publish tab's shared SCROLLING LIVE-LOG console —
3
+ * the "CI build log" panel reused across ALL FOUR long-running flows (build /
4
+ * integrate / rebuild / ship). Where `buildStream.ts` owns the build-flight
5
+ * stage-RAIL (checkmarks), this owns the LOG: one human-readable, timestamped line
6
+ * appended per server-pushed `phase` event, then a terminal ✓/✗ line.
7
+ *
8
+ * It is transport-agnostic (it consumes already-parsed {@link SseEvent}s from
9
+ * `parseSseChunk`) and flow-agnostic: ONE phase→label map covers every phase any of
10
+ * the four routes emits, so the SAME console renders an integrate merge, a ship
11
+ * promote, or a build read with no per-flow branching. The terminal-line extractor
12
+ * reads the union of the four routes' honest result shapes (build's
13
+ * `ok`/`outcome`/`errors`, the queue's `ok`/`queueState`/`statusMessage`/`reason`,
14
+ * ship's `ok`/`state`/`message`) without any one flow's assumptions leaking in.
15
+ *
16
+ * The DOM wiring that appends these to a scrollable `<div>` and auto-scrolls is
17
+ * verified on a live preview, per this codebase's client-island convention (see
18
+ * `reviewReleaseWorkspace/controller.ts`). Only this pure formatting is unit-tested.
19
+ */
20
+ import type { SseEvent } from "./buildStream.js";
21
+
22
+ /**
23
+ * The human label for every `phase` a streamed route emits, across all four flows.
24
+ * Deliberately honest-but-neutral wording so the SAME line reads true whether the
25
+ * phase came from a candidate build or an aggregate rebuild.
26
+ */
27
+ export const STREAM_PHASE_LABEL: Record<string, string> = {
28
+ // Accept-to-preview (integrate) — the queue's merge boundary.
29
+ merging: "Merging your change into the shared preview…",
30
+ // Build (candidate) — head resolution.
31
+ resolving: "Locating the pull request's latest commit…",
32
+ // Shared reconcile boundaries (build candidate + aggregate integrate/rebuild).
33
+ reading: "Reading and validating your store's files…",
34
+ publishing: "Building and publishing this version…",
35
+ checking: "Re-running the combined evidence checks…",
36
+ // Publish-live (ship) — the orchestrator's post-gate boundaries.
37
+ pinning: "Pinning the exact reviewed build…",
38
+ promoting: "Promoting the reviewed build to your live channel…",
39
+ verifying: "Verifying your live store serves the new build…",
40
+ recording: "Recording the release in your ledger…",
41
+ };
42
+
43
+ /**
44
+ * The label for one phase: a server-supplied `label` wins (a route may send a more
45
+ * specific line, e.g. build's "Locating the latest commit for PR #12…"), else the
46
+ * shared map, else `null` for an unknown phase (the caller ignores it — never
47
+ * fabricates a line for a phase it doesn't understand).
48
+ */
49
+ export function phaseLogLabel(phase: string, serverLabel?: string | null): string | null {
50
+ if (serverLabel && serverLabel.trim()) return serverLabel.trim();
51
+ return STREAM_PHASE_LABEL[phase] ?? null;
52
+ }
53
+
54
+ /** `m:ss` elapsed, matching the dialog's own elapsed-clock format. Clamps negatives to 0. */
55
+ export function formatElapsed(ms: number): string {
56
+ const s = Math.max(0, Math.floor(ms / 1000));
57
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
58
+ }
59
+
60
+ /**
61
+ * The log line for a `phase` event, or `null` to skip (unknown phase / not a phase
62
+ * event). Shape: `[m:ss] <label>`. Pure — the console appends the returned string.
63
+ */
64
+ export function logLineForPhase(
65
+ ev: SseEvent,
66
+ elapsedMs: number,
67
+ ): string | null {
68
+ if (ev.event !== "phase") return null;
69
+ let parsed: unknown = null;
70
+ try {
71
+ parsed = JSON.parse(ev.data);
72
+ } catch {
73
+ return null;
74
+ }
75
+ const phase = parsed && typeof (parsed as { phase?: unknown }).phase === "string"
76
+ ? (parsed as { phase: string }).phase
77
+ : null;
78
+ if (!phase) return null;
79
+ const serverLabel =
80
+ parsed && typeof (parsed as { label?: unknown }).label === "string"
81
+ ? (parsed as { label: string }).label
82
+ : null;
83
+ const label = phaseLogLabel(phase, serverLabel);
84
+ if (!label) return null;
85
+ return `[${formatElapsed(elapsedMs)}] ${label}`;
86
+ }
87
+
88
+ /** A terminal result across the four flows — the union of their honest shapes. */
89
+ export interface StreamTerminalResult {
90
+ ok?: boolean;
91
+ /** build */
92
+ outcome?: string | null;
93
+ errors?: string[] | null;
94
+ error?: string | null;
95
+ /** integrate/rebuild */
96
+ queueState?: string | null;
97
+ statusMessage?: string | null;
98
+ reason?: string | null;
99
+ /** ship */
100
+ state?: string | null;
101
+ message?: string | null;
102
+ }
103
+
104
+ /** True iff the terminal result is a clean success, reading each flow's own success signal. */
105
+ export function isStreamResultOk(result: StreamTerminalResult): boolean {
106
+ if (result.state !== undefined && result.state !== null) return result.state === "shipped";
107
+ if (result.ok === true) return true;
108
+ return result.outcome === "ok";
109
+ }
110
+
111
+ /** A SHORT one-line reason for a non-ok terminal (the full detail lives in the outcome phase). */
112
+ function streamResultReason(result: StreamTerminalResult): string {
113
+ const first = (v: string[] | null | undefined) => (v && v.length ? v[0] : "");
114
+ const candidate =
115
+ result.statusMessage ||
116
+ result.message ||
117
+ first(result.errors) ||
118
+ result.error ||
119
+ (result.outcome === "blocked" ? "a check blocked this build" : "") ||
120
+ result.reason ||
121
+ result.queueState ||
122
+ result.state ||
123
+ "it did not complete";
124
+ const line = String(candidate).replace(/\s+/g, " ").trim();
125
+ return line.length > 160 ? line.slice(0, 157) + "…" : line;
126
+ }
127
+
128
+ /**
129
+ * The terminal log line for a `result` event: `{ ok, line }`. `line` is
130
+ * `[m:ss] ✓ Done.` on success, `[m:ss] ✗ Stopped — <short reason>` otherwise. Pure.
131
+ */
132
+ export function terminalLogLine(
133
+ result: StreamTerminalResult,
134
+ elapsedMs: number,
135
+ ): { ok: boolean; line: string } {
136
+ const ok = isStreamResultOk(result);
137
+ const stamp = `[${formatElapsed(elapsedMs)}]`;
138
+ return {
139
+ ok,
140
+ line: ok ? `${stamp} ✓ Done.` : `${stamp} ✗ Stopped — ${streamResultReason(result)}`,
141
+ };
142
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The merchant-facing version counter — a monotonic, human "v1, v2, …" label for
3
+ * the tenant's published versions, DERIVED from the live-promotion history so it is
4
+ * never fabricated and never the raw SHA (which is meaningless to a merchant).
5
+ *
6
+ * A version's number is its rank by FIRST promotion to `live`: the earliest-ever
7
+ * promoted sha is v1, the next distinct sha is v2, and so on. This has the two
8
+ * properties merchants expect:
9
+ * - monotonic increasing — a newer publish always gets a higher number; and
10
+ * - stable for a given sha — a rollback re-points `live` at a prior version (per
11
+ * `PromotionEvent`'s contract a plain rollback is not even recorded, and a
12
+ * re-promote keeps the sha's ORIGINAL first-promotion rank), so "rolled back to
13
+ * v3" reads honestly and a subsequent fresh publish becomes v(N+1), never
14
+ * reusing a retired number.
15
+ *
16
+ * Pure (no I/O) so it unit-tests without a Worker/KV, and shared by both the
17
+ * live-version resolver (the "Live version" metric) and the versions-panel view so
18
+ * the two surfaces always agree on a version's number.
19
+ */
20
+ import type { PromotionEvent } from "@tot/public-runtime";
21
+
22
+ /**
23
+ * Map each promoted versionId → its 1-based version number, ranked by earliest
24
+ * promotion time. Input order is irrelevant (the rank is computed from the `at`
25
+ * timestamps), so it accepts the newest-first list `listPromotions` returns as-is.
26
+ * A tie on `at` between two distinct shas is broken by versionId for determinism.
27
+ */
28
+ export function assignVersionNumbers(promotions: PromotionEvent[]): Map<string, number> {
29
+ const earliest = new Map<string, string>();
30
+ for (const p of promotions) {
31
+ const seen = earliest.get(p.versionId);
32
+ if (seen == null || p.at < seen) earliest.set(p.versionId, p.at);
33
+ }
34
+ const ordered = [...earliest.entries()].sort((a, b) =>
35
+ a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0,
36
+ );
37
+ const numbers = new Map<string, number>();
38
+ ordered.forEach(([versionId], i) => numbers.set(versionId, i + 1));
39
+ return numbers;
40
+ }
41
+
42
+ /**
43
+ * The version number for one versionId, or `null` when nothing in the promotion
44
+ * history matches (an unpromoted/unknown sha — the label falls back to the raw SHA).
45
+ */
46
+ export function versionNumberFor(
47
+ promotions: PromotionEvent[],
48
+ versionId: string | null | undefined,
49
+ ): number | null {
50
+ if (!versionId) return null;
51
+ return assignVersionNumbers(promotions).get(versionId) ?? null;
52
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Fixed-window KV rate limiter — the storefront's first throttle, built small
3
+ * and generic so any public POST can share it (reactions today; blog comments
4
+ * and other reader writes next).
5
+ *
6
+ * A request is counted into the bucket for the current fixed window
7
+ * (`floor(now / windowSeconds)`), keyed `<key>:<bucket>`. The counter TTLs out a
8
+ * window after it is first written, so old buckets self-expire — no sweep.
9
+ *
10
+ * Best-effort by construction: a KV read/write hiccup FAILS OPEN (returns
11
+ * `allowed: true`) and never throws. A rate limiter must never turn a KV blip
12
+ * into a rejected legitimate request; the abuse it guards against is low-stakes
13
+ * (mirrors the "never throws" discipline of `blog/viewCounts.ts`).
14
+ */
15
+
16
+ export interface RateLimitOptions {
17
+ /** Window length in seconds; the counter resets each window. */
18
+ windowSeconds: number;
19
+ /** Max requests permitted per key per window before rejecting. */
20
+ maxRequests: number;
21
+ }
22
+
23
+ /** The current fixed-window bucket index for `now` (ms epoch). */
24
+ function windowBucket(windowSeconds: number, nowMs: number): number {
25
+ return Math.floor(nowMs / 1000 / windowSeconds);
26
+ }
27
+
28
+ /**
29
+ * Count one request against `key` and report whether it is within the cap for
30
+ * the current window. Increment is a read-modify-write, so under concurrency a
31
+ * few increments can be lost to KV's last-write-wins — acceptable for a soft
32
+ * abuse throttle, not a hard quota. Fails OPEN on any KV error.
33
+ */
34
+ export async function checkRateLimit(
35
+ kv: KVNamespace | undefined,
36
+ key: string,
37
+ opts: RateLimitOptions,
38
+ nowMs: number = Date.now(),
39
+ ): Promise<{ allowed: boolean }> {
40
+ if (!kv) return { allowed: true };
41
+ try {
42
+ const bucket = windowBucket(opts.windowSeconds, nowMs);
43
+ const bucketKey = `${key}:${bucket}`;
44
+ const raw = await kv.get(bucketKey);
45
+ const current = raw == null ? 0 : Number.parseInt(raw, 10);
46
+ const count = Number.isFinite(current) && current >= 0 ? current : 0;
47
+ if (count >= opts.maxRequests) return { allowed: false };
48
+ await kv.put(bucketKey, String(count + 1), { expirationTtl: opts.windowSeconds });
49
+ return { allowed: true };
50
+ } catch {
51
+ return { allowed: true };
52
+ }
53
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Canonical RAW chrome renderer (unit F3).
2
+ * Canonical RAW chrome renderer.
3
3
  *
4
4
  * ONE platform-shipped chrome wrapper, driven entirely by a tenant's `chrome.json`
5
- * (typed `TenantChrome`, lib/chrome/model.ts) — CONTENT ONLY. It replaces the three
5
+ * (typed `ChromeConfig`, lib/chrome/model.ts) — CONTENT ONLY. It replaces the three
6
6
  * divergent, hand-authored `content/<tenant>/chrome.html` documents with a single
7
7
  * generator, so chrome lives in ONE place and a tenant supplies only data. The
8
8
  * output is a full wrapper document carrying the splice markers the rawMarketingHtml
9
9
  * pipeline understands (`<!--PAGE_HEAD-->` / `<!--PAGE_BODY-->`), so body-only page
10
10
  * fragments get wrapped exactly as before (see lib/rawMarketingHtml.ts,
11
- * wrapFragmentInChrome). Per decisions:
12
- * - raw-chrome-single-stylesheet: the wrapper LINKS the platform stylesheet
13
- * (/shared/commerce-chrome.css, unit F2) and emits NO inline chrome CSS — every
11
+ * wrapFragmentInChrome). Two governing constraints:
12
+ * - Single stylesheet: the wrapper LINKS the platform stylesheet
13
+ * (/shared/commerce-chrome.css) and emits NO inline chrome CSS — every
14
14
  * color/space/type comes from theme tokens the tenant's linked *-theme.css sets.
15
- * - materialization-compat: this is a deterministic server-side string; the spliced
15
+ * - Materialization-compatible: this is a deterministic server-side string; the spliced
16
16
  * document is a stable, self-contained artifact SEOMaterialize can capture (the
17
17
  * behavior JS is progressive enhancement over already-rendered markup).
18
18
  *
19
- * The markup mirrors, class-for-class, the semantic markup the F2 stylesheet styles
19
+ * The markup mirrors, class-for-class, the semantic markup the shared chrome stylesheet styles
20
20
  * (`.site-header`, `.header-row`, `.primary-nav`, `.mega`, `.site-footer`, …) — the
21
21
  * SAME DOM the retired tenant chrome.html files carried, now generated from config.
22
22
  *
@@ -25,17 +25,23 @@
25
25
  * defense-in-depth, not a sanitizer for untrusted input.
26
26
  */
27
27
  import { CHROME_BODY_MARKER, CHROME_HEAD_MARKER } from "./rawMarketingHtml.js";
28
+ import { isCurrentNavHref } from "./chrome/navCurrent.js";
28
29
  import type {
29
30
  ChromeAgeGate,
30
31
  ChromeAnnouncement,
32
+ ChromeAssignments,
31
33
  ChromeBrand,
34
+ ChromeConfig,
32
35
  ChromeFooter,
36
+ ChromeHeader,
33
37
  ChromeLink,
34
38
  ChromeNavItem,
35
39
  ChromeUtilityNav,
40
+ HeaderVariant,
36
41
  SocialLink,
37
- TenantChrome,
38
42
  } from "./chrome/model.js";
43
+ import { resolveChromeAssignment } from "./chrome/model.js";
44
+ import { resolveHeaderVariant } from "./chrome/variant.js";
39
45
 
40
46
  /** HTML-escape text/attribute content. Covers the five significant characters. */
41
47
  function esc(value: string): string {
@@ -177,8 +183,18 @@ function renderUtilityNav(u: ChromeUtilityNav): string {
177
183
  );
178
184
  }
179
185
 
180
- /** A primary-nav item — flat link, or a mega panel when it has columns/children. */
181
- function renderNavItem(item: ChromeNavItem): string {
186
+ /**
187
+ * A primary-nav item flat link, or a mega panel when it has columns/children.
188
+ *
189
+ * `currentPath`, when given, is the tenant-relative request path (e.g. "/" or
190
+ * "/collections/all") in the SAME unprefixed form the config's own `href`
191
+ * values are authored in — basePath prefixing happens later, as a single
192
+ * pass over the whole composed document (rawMarketingHtml.ts,
193
+ * prefixTenantLinks), so comparing here before that pass keeps both sides
194
+ * unprefixed and exact-match-only (see navCurrent.ts). Omitted ⇒ no item is
195
+ * ever marked current (callers that don't know the request path, e.g. tests).
196
+ */
197
+ function renderNavItem(item: ChromeNavItem, currentPath?: string): string {
182
198
  const columns = item.columns?.length
183
199
  ? item.columns
184
200
  : item.children?.length
@@ -189,14 +205,23 @@ function renderNavItem(item: ChromeNavItem): string {
189
205
  const badge = item.badge ? `<span class="new-badge">${esc(item.badge)}</span>` : "";
190
206
  const caret = hasPanel ? `<span class="caret" aria-hidden="true">▾</span>` : "";
191
207
  const haspopup = hasPanel ? ' aria-haspopup="true"' : "";
192
- const link = `<a${linkClass} href="${esc(item.href)}"${haspopup}>${esc(item.label)}${badge}${caret}</a>`;
208
+ const itemCurrent = currentPath !== undefined && isCurrentNavHref(currentPath, item.href);
209
+ const ariaCurrent = itemCurrent ? ' aria-current="page"' : "";
210
+ const link = `<a${linkClass} href="${esc(item.href)}"${haspopup}${ariaCurrent}>${esc(item.label)}${badge}${caret}</a>`;
193
211
  if (!hasPanel) return `<li class="nav-item">${link}</li>`;
194
212
 
195
213
  const wide = columns.length > 1 ? " mega--wide" : "";
196
214
  const cols = columns
197
215
  .map((c) => {
198
216
  const heading = c.title ? `<h4>${esc(c.title)}</h4>` : "";
199
- const links = c.links.map((l) => `<a href="${esc(l.href)}"${relFor(l)}>${esc(l.label)}</a>`).join("");
217
+ const links = c.links
218
+ .map((l) => {
219
+ const childCurrent =
220
+ currentPath !== undefined && isCurrentNavHref(currentPath, l.href, l.external);
221
+ const childAriaCurrent = childCurrent ? ' aria-current="page"' : "";
222
+ return `<a href="${esc(l.href)}"${relFor(l)}${childAriaCurrent}>${esc(l.label)}</a>`;
223
+ })
224
+ .join("");
200
225
  return `<div class="mega__col">${heading}${links}</div>`;
201
226
  })
202
227
  .join("");
@@ -204,46 +229,73 @@ function renderNavItem(item: ChromeNavItem): string {
204
229
  return `<li class="nav-item">${link}${mega}</li>`;
205
230
  }
206
231
 
207
- function renderHeader(chrome: TenantChrome): string {
208
- const search = chrome.search
209
- ? `<form class="header-search" action="${esc(chrome.search.action ?? "/search")}" method="get" role="search">\n` +
210
- ` <label class="sr-only" for="hdr-q">${esc(chrome.search.label ?? "Search")}</label>\n` +
211
- ` <input id="hdr-q" type="search" name="q" placeholder="${esc(chrome.search.placeholder ?? "Search")}" autocomplete="off">\n` +
232
+ /**
233
+ * `variant` is the RESOLVED header variant for this page — the page-assignment
234
+ * override applied over `header.variant` by the caller. It is passed in rather
235
+ * than read off `header` so the rendered markup and its `data-variant` can
236
+ * never disagree with the variant the assignment actually decided.
237
+ */
238
+ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?: string): string {
239
+ const search = header.search
240
+ ? `<form class="header-search" action="${esc(header.search.action ?? "/search")}" method="get" role="search">\n` +
241
+ ` <label class="sr-only" for="hdr-q">${esc(header.search.label ?? "Search")}</label>\n` +
242
+ ` <input id="hdr-q" type="search" name="q" placeholder="${esc(header.search.placeholder ?? "Search")}" autocomplete="off">\n` +
212
243
  ` <button type="submit" aria-label="Search">${ICON_SEARCH}</button>\n` +
213
244
  ` </form>`
214
245
  : "";
215
- const memberCue = chrome.memberCue
216
- ? `<a class="member-cue" href="${esc(chrome.memberCue.href)}" aria-label="${esc(chrome.memberCue.label)}">${esc(chrome.memberCue.label)}</a>`
246
+ const memberCue = header.memberCue
247
+ ? `<a class="member-cue" href="${esc(header.memberCue.href)}" aria-label="${esc(header.memberCue.label)}">${esc(header.memberCue.label)}</a>`
217
248
  : "";
218
- const account = chrome.accountHref
219
- ? `<a href="${esc(chrome.accountHref)}" aria-label="Account">${ICON_ACCOUNT}</a>`
249
+ const account = header.accountHref
250
+ ? `<a href="${esc(header.accountHref)}" aria-label="Account">${ICON_ACCOUNT}</a>`
220
251
  : "";
221
- const cart = chrome.cartHref
222
- ? `<a class="cart" href="${esc(chrome.cartHref)}" aria-label="Cart">${ICON_CART}<span class="count">0</span></a>`
252
+ const cart = header.cartHref
253
+ ? `<a class="cart" href="${esc(header.cartHref)}" aria-label="Cart">${ICON_CART}<span class="count">0</span></a>`
223
254
  : "";
224
- const hamburger =
225
- `<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primaryNav"><span></span><span></span><span></span></button>`;
226
- const navItems = chrome.nav.map(renderNavItem).join("\n ");
255
+ // "minimal" (e.g. checkout/policy pages): brand + a reduced action set only —
256
+ // no primary nav bar, no search, no mobile-menu toggle. "primary": the full header.
257
+ const minimal = variant === "minimal";
258
+ // Header CTAs, on the same `!minimal` rule the Astro SiteHeader applies, so the
259
+ // two surfaces render the same governed config identically. Each carries its
260
+ // stable `id` as `data-cta-id`, so the governed identity is present in the
261
+ // served DOM rather than implied by label or href.
262
+ const ctas =
263
+ minimal || !header.ctas?.length
264
+ ? ""
265
+ : header.ctas
266
+ .map(
267
+ (c) =>
268
+ `<a class="header-cta header-cta--${esc(c.variant ?? "primary")}" ` +
269
+ `data-cta-id="${esc(c.id)}" href="${esc(c.href)}">${esc(c.label)}</a>`,
270
+ )
271
+ .join("");
272
+ const hamburger = minimal
273
+ ? ""
274
+ : `<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primaryNav"><span></span><span></span><span></span></button>`;
275
+ const navItems = header.nav.map((item) => renderNavItem(item, currentPath)).join("\n ");
276
+ const navBar = minimal
277
+ ? ""
278
+ : ` <nav class="primary-nav-bar" aria-label="Primary">\n` +
279
+ ` <div class="container">\n` +
280
+ ` <nav class="primary-nav" id="primaryNav">\n` +
281
+ ` <ul>\n ${navItems}\n </ul>\n` +
282
+ ` </nav>\n` +
283
+ ` </div>\n` +
284
+ ` </nav>\n`;
227
285
  return (
228
- `<header class="site-header">\n` +
286
+ `<header class="site-header" data-variant="${esc(variant)}">\n` +
229
287
  ` <div class="container header-row">\n` +
230
- ` ${renderBrand(chrome.brand)}\n` +
231
- (search ? ` ${search}\n` : "") +
232
- ` <div class="header-actions">${memberCue}${account}${cart}${hamburger}</div>\n` +
288
+ ` ${renderBrand(header.brand)}\n` +
289
+ (!minimal && search ? ` ${search}\n` : "") +
290
+ ` <div class="header-actions">${memberCue}${ctas}${account}${cart}${hamburger}</div>\n` +
233
291
  ` </div>\n` +
234
- ` <nav class="primary-nav-bar" aria-label="Primary">\n` +
235
- ` <div class="container">\n` +
236
- ` <nav class="primary-nav" id="primaryNav">\n` +
237
- ` <ul>\n ${navItems}\n </ul>\n` +
238
- ` </nav>\n` +
239
- ` </div>\n` +
240
- ` </nav>\n` +
292
+ navBar +
241
293
  `</header>`
242
294
  );
243
295
  }
244
296
 
245
297
  /**
246
- * Age-gate config, embedded as JSON (unit C1). The CHROME_BEHAVIOR_JS mechanism
298
+ * Age-gate config, embedded as JSON. The CHROME_BEHAVIOR_JS mechanism
247
299
  * reads this and builds the dialog itself inside a closed shadow root — the same
248
300
  * tamper-resistance pattern components/islands/IsolatedAgeGate.tsx uses for
249
301
  * component pages (see its header comment). `<` is escaped so an authored value
@@ -335,9 +387,16 @@ function renderFooter(footer: ChromeFooter): string {
335
387
  * — a single canonical source, never copy-pasted per tenant. Each block is guarded on
336
388
  * the presence of its elements, so a wrapper that omits the announce bar / age gate
337
389
  * runs the remaining behavior harmlessly. Progressive enhancement: the markup already
338
- * works without it (nav is visible, gate defaults open only when JS enables it).
390
+ * works without it (nav is a normal in-flow stacked list at mobile widths — see the
391
+ * `.js-nav` gate in commerce-chrome.css's max-width:860px block — and the age gate
392
+ * defaults open only when JS enables it).
339
393
  */
340
394
  export const CHROME_BEHAVIOR_JS = `(function () {
395
+ // Marks the document as JS-enabled so commerce-chrome.css can turn the
396
+ // always-reachable stacked mobile nav into an off-canvas drawer (progressive
397
+ // enhancement, not a requirement — see the .js-nav rules there).
398
+ document.documentElement.classList.add('js-nav');
399
+
341
400
  // Mobile nav drawer
342
401
  var burger = document.getElementById('hamburger');
343
402
  var nav = document.getElementById('primaryNav');
@@ -367,7 +426,7 @@ export const CHROME_BEHAVIOR_JS = `(function () {
367
426
  }
368
427
  }
369
428
 
370
- // 21+ age gate — shadow-isolated (unit C1). Only the host <div> lives in light
429
+ // 21+ age gate — shadow-isolated. Only the host <div> lives in light
371
430
  // DOM (data-tot-compliance, the same marker components/islands/IsolatedAgeGate.tsx
372
431
  // uses); its contents sit behind a closed shadow root, so tenant CSS/JS can't
373
432
  // select, restyle, or hide the dialog. A MutationObserver re-asserts the host the
@@ -444,11 +503,11 @@ export const CHROME_BEHAVIOR_JS = `(function () {
444
503
  }
445
504
  })();`;
446
505
 
447
- /** The platform-shipped chrome stylesheet every rendered wrapper links (unit F2). */
506
+ /** The platform-shipped chrome stylesheet every rendered wrapper links. */
448
507
  export const SHARED_CHROME_STYLESHEET = "/shared/commerce-chrome.css";
449
508
 
450
509
  /**
451
- * The platform-shipped RAW-PAGE-BODY marketing stylesheet (unit F5). Linked only
510
+ * The platform-shipped RAW-PAGE-BODY marketing stylesheet. Linked only
452
511
  * when a tenant opts in (chrome.sharedMarketingCss) and AFTER the chrome sheet, so
453
512
  * its page-level variants (e.g. .sec-head) win over chrome's home-section ones.
454
513
  */
@@ -480,8 +539,22 @@ function faviconMime(href: string): string {
480
539
  * `chromeWrapper` so a body-only fragment is spliced in and the whole thing is
481
540
  * nonce-stamped + base-path-prefixed. Optional sections (age gate, announcement,
482
541
  * utility nav, member cue, footer extras) render only when their config is present.
542
+ *
543
+ * `currentPath`, when given, is the tenant-relative request path in the same
544
+ * unprefixed form nav `href`s are authored in (see renderNavItem) — drives
545
+ * `aria-current="page"` on the primary-nav link that exactly matches it.
546
+ *
547
+ * `assignments`, when given, is the tenant's page-assignment manifest: the
548
+ * entry matching `currentPath` overrides `chrome.header.variant` for THIS page
549
+ * only. Resolution happens once, here, through the same fail-closed
550
+ * resolveHeaderVariant the Layout.astro prop path uses, and the resolved value
551
+ * is what reaches the markup.
483
552
  */
484
- export function renderRawChrome(chrome: TenantChrome): string {
553
+ export function renderRawChrome(
554
+ chrome: ChromeConfig,
555
+ currentPath?: string,
556
+ assignments?: ChromeAssignments | null,
557
+ ): string {
485
558
  const lang = esc(chrome.lang ?? "en");
486
559
  const head = chrome.head ?? {};
487
560
  const headLinks = [
@@ -491,20 +564,25 @@ export function renderRawChrome(chrome: TenantChrome): string {
491
564
  : "",
492
565
  ...(head.themeStylesheets ?? []).map((h) => `<link rel="stylesheet" href="${esc(h)}">`),
493
566
  `<link rel="stylesheet" href="${SHARED_CHROME_STYLESHEET}">`,
494
- // Opt-in raw-page-body layer, after the chrome sheet so its variants win (F5).
567
+ // Opt-in raw-page-body layer, after the chrome sheet so its variants win.
495
568
  chrome.sharedMarketingCss ? `<link rel="stylesheet" href="${SHARED_MARKETING_STYLESHEET}">` : "",
496
569
  ]
497
570
  .filter(Boolean)
498
571
  .join("\n");
499
572
 
573
+ const headerVariant = resolveHeaderVariant(
574
+ resolveChromeAssignment(assignments, currentPath)?.headerVariant,
575
+ chrome.header.variant,
576
+ );
577
+
500
578
  const skipHref = esc(chrome.skipLink?.href ?? "#main");
501
579
  const skipLabel = esc(chrome.skipLink?.label ?? "Skip to main content");
502
580
 
503
581
  const sections = [
504
582
  chrome.warningBar ? `<div class="topbar-warning"><p>${esc(chrome.warningBar)}</p></div>` : "",
505
- chrome.announcement ? renderAnnouncement(chrome.announcement) : "",
506
- chrome.utilityNav ? renderUtilityNav(chrome.utilityNav) : "",
507
- renderHeader(chrome),
583
+ chrome.header.announcement ? renderAnnouncement(chrome.header.announcement) : "",
584
+ chrome.header.utilityNav ? renderUtilityNav(chrome.header.utilityNav) : "",
585
+ renderHeader(chrome.header, headerVariant, currentPath),
508
586
  ].filter(Boolean);
509
587
 
510
588
  const trailing = [
@@ -15,6 +15,11 @@
15
15
  * injection is a deliberate string replace — robust for first-party documents, not a
16
16
  * sanitizer. Untrusted vendor HTML must be sandboxed/sanitized instead.
17
17
  */
18
+ import {
19
+ renderIntegrationPhases,
20
+ type EffectivePlan,
21
+ } from "@tot/public-runtime";
22
+
18
23
  export { rawHtmlPolicy } from "@tot/public-runtime";
19
24
 
20
25
  /**
@@ -64,6 +69,50 @@ export function wrapFragmentInChrome(fragment: string, chromeWrapper: string): s
64
69
  return out;
65
70
  }
66
71
 
72
+ /**
73
+ * Insert `snippet` at the very start of the document's `<head>` (or, absent a
74
+ * head, right after `<html>`, else at the document start) — so the renderer-owned
75
+ * head phases precede any tenant/app `<script>`/`<style>` in the served document.
76
+ * A body-only fragment with no wrapper has neither tag, so it is prepended.
77
+ */
78
+ function injectHeadStart(html: string, snippet: string): string {
79
+ // `(?:\s[^>]*)?>` matches `<head>` / `<head lang=…>` but NOT `<header>`.
80
+ const head = /<head(?:\s[^>]*)?>/i.exec(html);
81
+ if (head) {
82
+ const at = head.index + head[0].length;
83
+ return html.slice(0, at) + snippet + html.slice(at);
84
+ }
85
+ const htmlTag = /<html(?:\s[^>]*)?>/i.exec(html);
86
+ if (htmlTag) {
87
+ const at = htmlTag.index + htmlTag[0].length;
88
+ return html.slice(0, at) + snippet + html.slice(at);
89
+ }
90
+ return snippet + html;
91
+ }
92
+
93
+ /**
94
+ * Insert `snippet` at the very start of `<body>` (the body.start boundary), or —
95
+ * for a body-only fragment with no `<body>` — at the document start.
96
+ */
97
+ function injectBodyStart(html: string, snippet: string): string {
98
+ const body = /<body(?:\s[^>]*)?>/i.exec(html);
99
+ if (body) {
100
+ const at = body.index + body[0].length;
101
+ return html.slice(0, at) + snippet + html.slice(at);
102
+ }
103
+ return snippet + html;
104
+ }
105
+
106
+ /**
107
+ * Insert `snippet` immediately before `</body>` (the document.deferred boundary),
108
+ * or — for a body-only fragment with no `</body>` — at the document end.
109
+ */
110
+ function injectBodyEnd(html: string, snippet: string): string {
111
+ const at = html.toLowerCase().lastIndexOf("</body>");
112
+ if (at >= 0) return html.slice(0, at) + snippet + html.slice(at);
113
+ return html + snippet;
114
+ }
115
+
67
116
  /** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
68
117
  export function nonceInlineTags(html: string, nonce: string): string {
69
118
  return html
@@ -79,7 +128,7 @@ export function nonceInlineTags(html: string, nonce: string): string {
79
128
  * must NOT get the tenant prefix:
80
129
  * - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
81
130
  * - `/shared/…` — platform-shipped shared assets (e.g. the shared commerce-chrome
82
- * stylesheet, unit F2) that every tenant links.
131
+ * stylesheet) that every tenant links.
83
132
  * Asset `src="…"` isn't matched at all (href-only).
84
133
  */
85
134
  export function prefixTenantLinks(html: string, basePath: string): string {
@@ -100,7 +149,19 @@ export function prefixTenantLinks(html: string, basePath: string): string {
100
149
  */
101
150
  export function rawHtmlResponse(
102
151
  html: string,
103
- opts: { nonce: string; basePath: string; chromeWrapper?: string | null },
152
+ opts: {
153
+ nonce: string;
154
+ basePath: string;
155
+ chromeWrapper?: string | null;
156
+ /**
157
+ * The resolved effective plan for this request — the raw-mount half of the
158
+ * "one engine, two mounts" contract (Layout.astro is the other). The SAME
159
+ * render engine emits the renderer-owned phases into this document, so its
160
+ * phase markup is byte-identical to Layout's. Required: no render path may
161
+ * silently omit it. Nonces are stamped per response, never serialized (ADR 0018).
162
+ */
163
+ plan: EffectivePlan;
164
+ },
104
165
  ): Response {
105
166
  let composed =
106
167
  isFullDocument(html) || !opts.chromeWrapper
@@ -130,7 +191,17 @@ export function rawHtmlResponse(
130
191
  ? composed.replace("</body>", `${widget}</body>`)
131
192
  : `${composed}${widget}`;
132
193
  }
133
- const doc = prefixTenantLinks(nonceInlineTags(composed, opts.nonce), opts.basePath);
194
+ // Stamp the page nonce onto the TENANT's inline tags first, then splice the render
195
+ // engine's phase markup — which already carries the nonce — so the engine bytes are
196
+ // never double-stamped and match Layout.astro's mount exactly. `opts.nonce` is the
197
+ // per-response nonce placeholder; the middleware substitutes a fresh nonce at serve
198
+ // time (ADR 0018), so the same engine output feeds both mounts identically.
199
+ composed = nonceInlineTags(composed, opts.nonce);
200
+ const phases = renderIntegrationPhases(opts.plan, opts.nonce);
201
+ composed = injectHeadStart(composed, phases.head);
202
+ composed = injectBodyStart(composed, phases.bodyStart);
203
+ composed = injectBodyEnd(composed, phases.bodyDeferred);
204
+ const doc = prefixTenantLinks(composed, opts.basePath);
134
205
  return new Response(doc, {
135
206
  headers: { "content-type": "text/html; charset=utf-8" },
136
207
  });