@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,325 @@
1
+ /**
2
+ * Client controller for the merchant Review-and-Release workspace (`review-release.astro`).
3
+ *
4
+ * Reads server-stamped context from the root element, polls `GET /api/changes`,
5
+ * derives the two-decision view model with the pure `@/lib/publish/reviewRelease`
6
+ * seam, renders it with the pure `render.ts`, and wires the ONLY two actions —
7
+ * "Add to next release" and "Publish now". It owns the browser-only concerns the
8
+ * pure modules can't: poll cadence, the `shipInFlight` "Publishing…" transient, the
9
+ * publish confirmation dialog (focus trap + Esc + focus return), and ARIA-live
10
+ * discipline (one announcement per REAL state transition, never per poll tick).
11
+ *
12
+ * Not unit-tested (the storefront test env is `node`, no DOM) — the render + action
13
+ * logic it composes IS tested; this glue is verified on a live preview.
14
+ */
15
+ import {
16
+ buildReviewReleaseViewModel,
17
+ formatReleaseDate,
18
+ READ_ONLY_NOTE,
19
+ UNDO_TOAST_COPY,
20
+ type ReviewReleaseViewModel,
21
+ type ReviewReleaseContext,
22
+ type ChangesPayload,
23
+ } from "@/lib/publish/reviewRelease";
24
+ import { renderWorkspace, type LiveReleaseView } from "./render.js";
25
+ import { addChangeToRelease, publishNextRelease } from "./actions.js";
26
+
27
+ interface WorkspaceContext {
28
+ changesEndpoint: string;
29
+ acceptEndpoint: string;
30
+ shipEndpoint: string;
31
+ buildEndpoint: string;
32
+ liveHref: string;
33
+ canShip: boolean;
34
+ internalTestStore: boolean;
35
+ storeName: string;
36
+ pollMs: number;
37
+ slowThresholdMs: number;
38
+ }
39
+
40
+ const DEFAULT_POLL_MS = 8000;
41
+
42
+ export function initReviewReleaseWorkspace(): void {
43
+ const root = document.querySelector<HTMLElement>("[data-rr-root]");
44
+ if (!root) return;
45
+ const workspace = root.querySelector<HTMLElement>("[data-rr-workspace]");
46
+ const announcer = root.querySelector<HTMLElement>("[data-rr-announce]");
47
+ const toast = root.querySelector<HTMLElement>("[data-rr-toast]");
48
+ if (!workspace) return;
49
+
50
+ const ctx: WorkspaceContext = {
51
+ changesEndpoint: root.dataset.changesEndpoint ?? "/api/changes",
52
+ acceptEndpoint: root.dataset.acceptEndpoint ?? "/api/changes/accept",
53
+ shipEndpoint: root.dataset.shipEndpoint ?? "/api/changes/ship",
54
+ buildEndpoint: root.dataset.buildEndpoint ?? "/api/preview/build",
55
+ liveHref: root.dataset.liveHref ?? "/",
56
+ canShip: root.dataset.canShip === "true",
57
+ internalTestStore: root.dataset.internalTestStore === "true",
58
+ storeName: root.dataset.storeName ?? "your store",
59
+ pollMs: Number(root.dataset.pollMs) || DEFAULT_POLL_MS,
60
+ slowThresholdMs: Number(root.dataset.slowThresholdMs) || 120_000,
61
+ };
62
+
63
+ let shipInFlight = false;
64
+ let publishSucceeded = false;
65
+ let prevVm: ReviewReleaseViewModel | null = null;
66
+ let lastPayload: ChangesPayload | null = null;
67
+ let busy = false; // an action POST is in flight — suppress a racing re-render
68
+
69
+ // ── Rendering ──────────────────────────────────────────────────────────────
70
+ function render(payload: ChangesPayload): void {
71
+ lastPayload = payload;
72
+ const modelCtx: ReviewReleaseContext = {
73
+ canShip: ctx.canShip,
74
+ internalTestStore: ctx.internalTestStore,
75
+ storeName: ctx.storeName,
76
+ shipInFlight,
77
+ preparingSlowThresholdMs: ctx.slowThresholdMs,
78
+ };
79
+ const vm = buildReviewReleaseViewModel(payload, modelCtx);
80
+ const current = payload.current ?? {};
81
+ const live: LiveReleaseView = {
82
+ versionId: current.versionId ?? null,
83
+ publishedDate: formatReleaseDate(current.createdAt ?? null),
84
+ liveHref: ctx.liveHref,
85
+ };
86
+ workspace!.innerHTML = renderWorkspace(vm, { liveHref: ctx.liveHref, live, publishSucceeded });
87
+ announceTransitions(prevVm, vm);
88
+ prevVm = vm;
89
+ triggerPreparingWork(vm);
90
+ }
91
+
92
+ // ── "Preparing" must mean something real is happening, not just a claim ────────
93
+ // A change sits in "preparing" (queued/reconciling/not-built) until something
94
+ // actually invokes the build/reconcile pipeline — no standing background job
95
+ // does that on its own. Kick it off automatically, once per change per page
96
+ // load, so the merchant's "we're working on this" copy is true rather than a
97
+ // dead label waiting on an external webhook that may never arrive.
98
+ const triggeredPreparing = new Set<string>();
99
+ function triggerPreparingWork(vm: ReviewReleaseViewModel): void {
100
+ if (!ctx.canShip) return;
101
+ for (const c of vm.changes) {
102
+ if (c.state !== "preparing") continue;
103
+ const key = c.changeId ?? (c.prNumber != null ? `pr:${c.prNumber}` : null);
104
+ if (!key || triggeredPreparing.has(key)) continue;
105
+ triggeredPreparing.add(key);
106
+ const body: Record<string, unknown> = {};
107
+ if (c.prNumber != null) body.pr = c.prNumber;
108
+ if (c.changeId) body.changeId = c.changeId;
109
+ fetch(ctx.buildEndpoint, {
110
+ method: "POST",
111
+ headers: { "content-type": "application/json" },
112
+ body: JSON.stringify(body),
113
+ }).catch(() => {}); // best-effort; the next poll just still shows "Preparing" if it failed
114
+ }
115
+ }
116
+
117
+ function renderReadOnlyFallback(): void {
118
+ workspace!.innerHTML =
119
+ `<section class="rr-section"><p class="rr-note">${escapeText(READ_ONLY_NOTE)}</p></section>`;
120
+ }
121
+
122
+ // ── Data ─────────────────────────────────────────────────────────────────────
123
+ async function load(): Promise<void> {
124
+ if (busy) return;
125
+ try {
126
+ const res = await fetch(ctx.changesEndpoint, { headers: { accept: "application/json" } });
127
+ if (res.status === 401 || res.status === 403) {
128
+ renderReadOnlyFallback();
129
+ return;
130
+ }
131
+ if (!res.ok) return; // transient; the next poll retries silently (no merchant error)
132
+ const payload = (await res.json()) as ChangesPayload;
133
+ // A completed ship clears the client transient once the server confirms live.
134
+ if (shipInFlight && payload.aggregate?.queueState === "shipped") shipInFlight = false;
135
+ render(payload);
136
+ } catch {
137
+ // Network blip — leave the last render in place; the poll loop retries.
138
+ }
139
+ }
140
+
141
+ // ── ARIA-live: one announcement per REAL transition ───────────────────────────
142
+ function announceTransitions(prev: ReviewReleaseViewModel | null, next: ReviewReleaseViewModel): void {
143
+ if (!announcer) return;
144
+ const msgs: string[] = [];
145
+ if (!prev || prev.nextRelease.state !== next.nextRelease.state) {
146
+ msgs.push(next.nextRelease.heading);
147
+ }
148
+ if (prev) {
149
+ const before = new Map(prev.changes.map((c) => [c.changeId ?? `pr:${c.prNumber}`, c.state]));
150
+ for (const c of next.changes) {
151
+ const key = c.changeId ?? `pr:${c.prNumber}`;
152
+ const wasState = before.get(key);
153
+ if (wasState && wasState !== c.state) {
154
+ const title = c.title?.trim() || (c.prNumber != null ? `Change #${c.prNumber}` : "A change");
155
+ msgs.push(`${title} is now ${c.label}.`);
156
+ }
157
+ }
158
+ }
159
+ if (msgs.length) setAnnouncement(msgs.join(" "));
160
+ }
161
+
162
+ function setAnnouncement(text: string): void {
163
+ if (!announcer) return;
164
+ announcer.textContent = "";
165
+ // Force SR pickup of a possibly-repeated message on the next frame.
166
+ window.requestAnimationFrame(() => {
167
+ announcer.textContent = text;
168
+ });
169
+ }
170
+
171
+ function showToast(text: string): void {
172
+ if (!toast) return;
173
+ toast.textContent = text;
174
+ toast.hidden = false;
175
+ window.setTimeout(() => {
176
+ toast.hidden = true;
177
+ }, 5000);
178
+ }
179
+
180
+ // ── Action wiring (event delegation; survives innerHTML re-renders) ────────────
181
+ workspace.addEventListener("click", (ev) => {
182
+ const target = ev.target as HTMLElement;
183
+ const addBtn = target.closest<HTMLButtonElement>("[data-rr-add]");
184
+ if (addBtn && !addBtn.disabled) {
185
+ void handleAdd(addBtn);
186
+ return;
187
+ }
188
+ if (target.closest("[data-rr-publish]")) {
189
+ openConfirm();
190
+ }
191
+ });
192
+
193
+ async function handleAdd(btn: HTMLButtonElement): Promise<void> {
194
+ const changeId = btn.dataset.changeId || null;
195
+ const prRaw = btn.dataset.prNumber;
196
+ const prNumber = prRaw && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
197
+ btn.disabled = true;
198
+ btn.setAttribute("aria-busy", "true");
199
+ busy = true;
200
+ const result = await addChangeToRelease(
201
+ { fetch: fetchLike, endpoint: ctx.acceptEndpoint },
202
+ { changeId, prNumber },
203
+ );
204
+ busy = false;
205
+ if (result.kind === "added") {
206
+ setAnnouncement("Added to your next release.");
207
+ showToast(UNDO_TOAST_COPY);
208
+ } else if (result.kind === "needs-attention") {
209
+ setAnnouncement("That change needs a developer before it can go live. We've let them know.");
210
+ } else {
211
+ setAnnouncement("We couldn't add that just now — it will reappear here if it's still ready.");
212
+ }
213
+ await load();
214
+ }
215
+
216
+ // ── Publish confirmation dialog (focus-trapped) ───────────────────────────────
217
+ const confirmEl = root.querySelector<HTMLElement>("[data-rr-confirm]");
218
+ const dialogEl = root.querySelector<HTMLElement>("[data-rr-confirm-dialog]");
219
+ const confirmHeadingEl = root.querySelector<HTMLElement>("[data-rr-confirm-heading]");
220
+ const confirmBodyEl = root.querySelector<HTMLElement>("[data-rr-confirm-body]");
221
+ const confirmOkEl = root.querySelector<HTMLButtonElement>("[data-rr-confirm-ok]");
222
+ const confirmCancelEl = root.querySelector<HTMLButtonElement>("[data-rr-confirm-cancel]");
223
+ let invokingButton: HTMLElement | null = null;
224
+
225
+ function openConfirm(): void {
226
+ if (!confirmEl || !prevVm?.nextRelease.confirm) return;
227
+ const confirm = prevVm.nextRelease.confirm;
228
+ if (confirmHeadingEl) confirmHeadingEl.textContent = confirm.heading;
229
+ if (confirmBodyEl) confirmBodyEl.textContent = confirm.body;
230
+ if (confirmOkEl) confirmOkEl.textContent = confirm.confirm;
231
+ if (confirmCancelEl) confirmCancelEl.textContent = confirm.cancel;
232
+ invokingButton = document.activeElement as HTMLElement;
233
+ confirmEl.hidden = false;
234
+ (confirmOkEl ?? dialogEl)?.focus();
235
+ document.addEventListener("keydown", onDialogKeydown, true);
236
+ }
237
+
238
+ function closeConfirm(): void {
239
+ if (!confirmEl) return;
240
+ confirmEl.hidden = true;
241
+ document.removeEventListener("keydown", onDialogKeydown, true);
242
+ invokingButton?.focus();
243
+ invokingButton = null;
244
+ }
245
+
246
+ function onDialogKeydown(ev: KeyboardEvent): void {
247
+ if (ev.key === "Escape") {
248
+ ev.preventDefault();
249
+ closeConfirm();
250
+ return;
251
+ }
252
+ if (ev.key === "Tab" && dialogEl) {
253
+ const focusable = dialogEl.querySelectorAll<HTMLElement>(
254
+ 'button, [href], input, [tabindex]:not([tabindex="-1"])',
255
+ );
256
+ const first = focusable[0];
257
+ const last = focusable[focusable.length - 1];
258
+ if (!first || !last) return;
259
+ const active = document.activeElement;
260
+ if (ev.shiftKey && active === first) {
261
+ ev.preventDefault();
262
+ last.focus();
263
+ } else if (!ev.shiftKey && active === last) {
264
+ ev.preventDefault();
265
+ first.focus();
266
+ }
267
+ }
268
+ }
269
+
270
+ confirmCancelEl?.addEventListener("click", closeConfirm);
271
+ confirmEl?.addEventListener("click", (ev) => {
272
+ if (ev.target === confirmEl) closeConfirm(); // backdrop click
273
+ });
274
+ confirmOkEl?.addEventListener("click", () => {
275
+ closeConfirm();
276
+ void handlePublish();
277
+ });
278
+
279
+ async function handlePublish(): Promise<void> {
280
+ shipInFlight = true;
281
+ publishSucceeded = false;
282
+ // Re-render immediately from the last payload so the release shows "Publishing…"
283
+ // while the go-live POST is in flight (the client-owned transient, contract §3).
284
+ if (lastPayload) render(lastPayload);
285
+ busy = true;
286
+ const result = await publishNextRelease({ fetch: fetchLike, endpoint: ctx.shipEndpoint });
287
+ busy = false;
288
+ if (result.kind === "published") {
289
+ shipInFlight = false;
290
+ publishSucceeded = true;
291
+ setAnnouncement("Your release is live.");
292
+ } else if (result.kind === "paywall") {
293
+ shipInFlight = false;
294
+ setAnnouncement("Publishing to your live store is part of the paid plan.");
295
+ } else if (result.kind === "not-ready") {
296
+ shipInFlight = false;
297
+ setAnnouncement("We're getting your next release ready. Nothing you need to do.");
298
+ } else if (result.kind === "publish-failed") {
299
+ shipInFlight = false;
300
+ setAnnouncement("We couldn't finish publishing. We're retrying — nothing you need to do.");
301
+ } else {
302
+ shipInFlight = false;
303
+ setAnnouncement("We couldn't finish publishing just now. We're retrying automatically.");
304
+ }
305
+ await load();
306
+ }
307
+
308
+ // ── Kickoff ───────────────────────────────────────────────────────────────────
309
+ void load();
310
+ window.setInterval(() => {
311
+ if (!confirmEl || confirmEl.hidden) void load();
312
+ }, ctx.pollMs);
313
+ }
314
+
315
+ /** Adapt the global `fetch` to the actions' minimal `FetchLike` shape. */
316
+ function fetchLike(
317
+ input: string,
318
+ init?: { method?: string; headers?: Record<string, string>; body?: string },
319
+ ): Promise<{ ok: boolean; status: number; json: () => Promise<unknown> }> {
320
+ return fetch(input, init);
321
+ }
322
+
323
+ function escapeText(value: string): string {
324
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
325
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Pure HTML rendering for the merchant Review-and-Release workspace.
3
+ *
4
+ * Consumes the already-decided `ReviewReleaseViewModel` (from the
5
+ * `@/lib/publish/reviewRelease` seam) and returns serializable HTML strings — no
6
+ * DOM, no I/O — so it is exhaustively node-testable (the storefront test env is
7
+ * `node`, not jsdom; interaction/focus is verified on a live preview). The client
8
+ * controller injects these strings and wires events; this module invents NO copy
9
+ * (the seam's view model is the single source of truth) and adds NO merchant
10
+ * action beyond the two the model carries.
11
+ */
12
+ import type {
13
+ ReviewReleaseViewModel,
14
+ ChangeCardView,
15
+ NextReleaseView,
16
+ MerchantPermissions,
17
+ } from "@/lib/publish/reviewRelease";
18
+ import { NO_CHANGES_COPY, PUBLISH_SUCCESS_COPY } from "@/lib/publish/reviewRelease";
19
+
20
+ /** Best-effort "what's live now" pointer (contract §9.7 history gap: no full history yet). */
21
+ export interface LiveReleaseView {
22
+ /** The live version id (short-rendered), or null when nothing is live. */
23
+ versionId: string | null;
24
+ /** Deterministic "Mon D, YYYY" published date, or "" when unknown. */
25
+ publishedDate: string;
26
+ /** basePath-aware href to the live store root. */
27
+ liveHref: string;
28
+ }
29
+
30
+ export interface WorkspaceRenderOptions {
31
+ /** basePath-aware href to the live store root (for the release "View your live store"). */
32
+ liveHref: string;
33
+ /** The current live release pointer, rendered as a persistent "Live" section. */
34
+ live: LiveReleaseView;
35
+ /** The post-publish success line + link is shown only right after a publish. */
36
+ publishSucceeded?: boolean;
37
+ }
38
+
39
+ /** HTML-escape an interpolated dynamic value (titles come from PR titles; store name from config). */
40
+ export function esc(value: string): string {
41
+ return value
42
+ .replace(/&/g, "&amp;")
43
+ .replace(/</g, "&lt;")
44
+ .replace(/>/g, "&gt;")
45
+ .replace(/"/g, "&quot;")
46
+ .replace(/'/g, "&#39;");
47
+ }
48
+
49
+ const READONLY_NOTE_ID = "rr-readonly-note";
50
+ const TESTSTORE_NOTE_ID = "rr-teststore-note";
51
+ const PAYWALL_NOTE_ID = "rr-paywall-note";
52
+
53
+ // ── Change cards ───────────────────────────────────────────────────────────────
54
+
55
+ /** The display title for a change: its human title, else a PR/change reference. */
56
+ function changeTitle(c: ChangeCardView): string {
57
+ if (c.title && c.title.trim()) return c.title.trim();
58
+ if (typeof c.prNumber === "number") return `Change #${c.prNumber}`;
59
+ if (c.changeId) return c.changeId;
60
+ return "Change";
61
+ }
62
+
63
+ /**
64
+ * One change row. The status chip always carries its state WORD (color-independent,
65
+ * a11y §8). The "Add to next release" button appears only on `ready-to-add`; when the
66
+ * viewer is read-only it is rendered `disabled` with an `aria-describedby` pointing at
67
+ * the read-only note (never hidden — contract §6). `needs-attention` carries no
68
+ * action at all — its subline already states the developer was notified.
69
+ */
70
+ export function renderChangeCard(c: ChangeCardView, perms: MerchantPermissions): string {
71
+ const title = esc(changeTitle(c));
72
+ const chip = `<span class="rr-chip rr-chip-${c.state}">${esc(c.label)}</span>`;
73
+ const subline = `<p class="rr-subline">${esc(c.subline)}</p>`;
74
+
75
+ let actions = "";
76
+ if (c.action && c.action.id === "add-to-next-release") {
77
+ const accessibleName = c.title ? `${c.action.label}: ${title}` : c.action.label;
78
+ if (perms.canAddAndPublish) {
79
+ actions += `<button type="button" class="rr-btn rr-btn-primary" data-rr-add`
80
+ + ` data-change-id="${esc(c.changeId ?? "")}" data-pr-number="${c.prNumber ?? ""}"`
81
+ + ` aria-label="${esc(accessibleName)}">${esc(c.action.label)}</button>`;
82
+ } else {
83
+ // Read-only: never hide the action — show it disabled with a programmatic reason.
84
+ actions += `<button type="button" class="rr-btn rr-btn-primary" disabled`
85
+ + ` aria-describedby="${READONLY_NOTE_ID}"`
86
+ + ` aria-label="${esc(accessibleName)}">${esc(c.action.label)}</button>`;
87
+ }
88
+ }
89
+ return (
90
+ `<li class="rr-change" data-rr-change data-state="${c.state}"`
91
+ + ` data-change-id="${esc(c.changeId ?? "")}" data-pr-number="${c.prNumber ?? ""}">`
92
+ + `<div class="rr-change-main"><div class="rr-change-head">`
93
+ + `<span class="rr-change-title">${title}</span>${chip}</div>${subline}</div>`
94
+ + `<div class="rr-change-actions">${actions}</div>`
95
+ + `</li>`
96
+ );
97
+ }
98
+
99
+ /** The "Changes to review" section. Honest zero-state when the queue is empty. */
100
+ export function renderChangesSection(vm: ReviewReleaseViewModel): string {
101
+ const items = vm.changes.map((c) => renderChangeCard(c, vm.permissions)).join("");
102
+ const body = vm.changes.length
103
+ ? `<ul class="rr-change-list" data-rr-change-list>${items}</ul>`
104
+ : `<p class="rr-empty" data-rr-changes-empty>${esc(NO_CHANGES_COPY)}</p>`;
105
+ const readOnlyNote = vm.permissions.readOnly
106
+ ? `<p class="rr-note" id="${READONLY_NOTE_ID}">${esc(vm.permissions.readOnlyNote ?? "")}</p>`
107
+ : "";
108
+ return (
109
+ `<section class="rr-section rr-changes" aria-labelledby="rr-changes-title">`
110
+ + `<h2 id="rr-changes-title" class="rr-section-title">Changes to review</h2>`
111
+ + readOnlyNote
112
+ + body
113
+ + `</section>`
114
+ );
115
+ }
116
+
117
+ // ── Next release ─────────────────────────────────────────────────────────────────
118
+
119
+ /** The "What's in this release" plain-title list (Ready to publish) — no sha/digest/counts. */
120
+ function renderContents(next: NextReleaseView): string {
121
+ if (next.state !== "ready-to-publish") return "";
122
+ const items = next.contents.items.length
123
+ ? `<ul class="rr-contents-list">${next.contents.items.map((t) => `<li>${esc(t)}</li>`).join("")}</ul>`
124
+ : `<p class="rr-note">${esc(next.contents.fallback)}</p>`;
125
+ return (
126
+ `<div class="rr-contents">`
127
+ + `<h3 class="rr-contents-title">${esc(next.contents.heading)}</h3>${items}</div>`
128
+ );
129
+ }
130
+
131
+ /**
132
+ * The next-release action row. `Publish now` appears ONLY in `ready-to-publish` and
133
+ * ONLY when the model resolved the publish action (owner/admin/ship, entitled,
134
+ * not a test store). In `ready-to-publish` WITHOUT the action, the button is replaced
135
+ * — never hidden — by the internal-test-store notice, the paywall upgrade prompt, or
136
+ * the read-only note, in that precedence. `live` offers only "View your live store".
137
+ */
138
+ function renderReleaseAction(
139
+ next: NextReleaseView,
140
+ perms: MerchantPermissions,
141
+ liveHref: string,
142
+ ): string {
143
+ if (next.action && next.action.id === "publish-now") {
144
+ return (
145
+ `<div class="rr-release-action">`
146
+ + `<button type="button" class="rr-btn rr-btn-primary rr-publish" data-rr-publish>`
147
+ + `${esc(next.action.label)}</button></div>`
148
+ );
149
+ }
150
+ if (next.action && next.action.id === "view-live-store") {
151
+ return (
152
+ `<div class="rr-release-action">`
153
+ + `<a class="rr-btn rr-btn-primary" href="${esc(liveHref)}" data-rr-view-live>`
154
+ + `${esc(next.action.label)}</a></div>`
155
+ );
156
+ }
157
+ // Ready-to-publish but the action was withheld — show WHY (never a bare hidden button).
158
+ if (next.state === "ready-to-publish") {
159
+ if (perms.internalTestStore) {
160
+ return (
161
+ `<div class="rr-release-action rr-blocked">`
162
+ + `<p class="rr-note" id="${TESTSTORE_NOTE_ID}" role="note">`
163
+ + `${esc(perms.internalTestStoreNote ?? "")}</p></div>`
164
+ );
165
+ }
166
+ if (perms.paywall.active) {
167
+ return (
168
+ `<div class="rr-release-action rr-paywall" data-rr-paywall>`
169
+ + `<p class="rr-note" id="${PAYWALL_NOTE_ID}">Publishing to your live store is part of the paid plan. `
170
+ + `Previewing and reviewing stays free.</p>`
171
+ + `<a class="rr-btn rr-btn-primary" href="mailto:success@tokenoftrust.com?subject=Upgrade%20store%20publishing"`
172
+ + ` data-rr-upgrade>Upgrade to publish</a></div>`
173
+ );
174
+ }
175
+ if (perms.readOnly) {
176
+ return (
177
+ `<div class="rr-release-action">`
178
+ + `<button type="button" class="rr-btn rr-btn-primary" disabled aria-describedby="${READONLY_NOTE_ID}">`
179
+ + `Publish now</button>`
180
+ + `<p class="rr-note" id="${READONLY_NOTE_ID}">${esc(perms.readOnlyNote ?? "")}</p></div>`
181
+ );
182
+ }
183
+ }
184
+ return "";
185
+ }
186
+
187
+ /** The next-release panel: heading + body (the state), the contents list, and the action. */
188
+ export function renderNextRelease(
189
+ next: NextReleaseView,
190
+ perms: MerchantPermissions,
191
+ liveHref: string,
192
+ publishSucceeded = false,
193
+ ): string {
194
+ const success = publishSucceeded
195
+ ? `<p class="rr-success" data-rr-publish-success>${esc(PUBLISH_SUCCESS_COPY.message)} `
196
+ + `<a href="${esc(liveHref)}">${esc(PUBLISH_SUCCESS_COPY.link)}</a></p>`
197
+ : "";
198
+ return (
199
+ `<section class="rr-section rr-release" data-rr-release data-state="${next.state}"`
200
+ + ` aria-labelledby="rr-release-title">`
201
+ + `<div class="rr-release-status">`
202
+ + `<h2 id="rr-release-title" class="rr-section-title">${esc(next.heading)}</h2>`
203
+ + `<p class="rr-release-body">${esc(next.body)}</p></div>`
204
+ + renderContents(next)
205
+ + success
206
+ + renderReleaseAction(next, perms, liveHref)
207
+ + `</section>`
208
+ );
209
+ }
210
+
211
+ // ── Live (best-effort "what's live now") ─────────────────────────────────────────
212
+
213
+ /**
214
+ * The "Live" section — the current live release. Best-effort: the open-queue
215
+ * `/api/changes` payload carries only the live-version pointer (no published
216
+ * history yet, contract §9.7), so this shows the live version + date + a link, not a
217
+ * historical list. Omitted entirely when nothing is live.
218
+ */
219
+ export function renderLiveSection(live: LiveReleaseView): string {
220
+ if (!live.versionId) return "";
221
+ const shortId = esc(live.versionId.slice(0, 12));
222
+ const when = live.publishedDate ? ` · Published ${esc(live.publishedDate)}` : "";
223
+ return (
224
+ `<section class="rr-section rr-live" aria-labelledby="rr-live-title">`
225
+ + `<h2 id="rr-live-title" class="rr-section-title">Live</h2>`
226
+ + `<p class="rr-live-body">Your store is serving <code class="rr-mono">${shortId}</code>${when}.</p>`
227
+ + `<a class="rr-btn rr-btn-ghost" href="${esc(live.liveHref)}">View your live store</a>`
228
+ + `</section>`
229
+ );
230
+ }
231
+
232
+ // ── Whole workspace ──────────────────────────────────────────────────────────────
233
+
234
+ /** Compose the three sections the controller injects into the workspace container. */
235
+ export function renderWorkspace(vm: ReviewReleaseViewModel, opts: WorkspaceRenderOptions): string {
236
+ return (
237
+ renderChangesSection(vm)
238
+ + renderNextRelease(vm.nextRelease, vm.permissions, opts.liveHref, opts.publishSucceeded)
239
+ + renderLiveSection(opts.live)
240
+ );
241
+ }
@@ -46,6 +46,7 @@
46
46
  * digest column (surfaced for a static tenant) differs.
47
47
  */
48
48
  import { reviewDashboardRevisionHref } from "@/lib/preview/reviewDashboard";
49
+ import { assignVersionNumbers } from "@/lib/publish/versionNumber";
49
50
  import {
50
51
  candidateEvidenceAuthority,
51
52
  classifyCandidateForRender,
@@ -57,8 +58,10 @@ import type { ReviewEnvironment, PromotionEvent } from "@tot/public-runtime";
57
58
  // ── Versions / promotion history ───────────────────────────────────────────
58
59
 
59
60
  /** The plain-language "what shipped in this version" model (same shape the staged
60
- * release summary uses — a lead sentence + one highlight per included change). */
61
+ * release summary uses — a name + a lead sentence + one highlight per included change). */
61
62
  export interface VersionReleaseSummary {
63
+ /** The human-readable release name (see `deriveReleaseName`), or null. */
64
+ name: string | null;
62
65
  lead: string;
63
66
  highlights: { title: string; detail: string }[];
64
67
  }
@@ -87,6 +90,12 @@ export interface VersionReleaseInfo {
87
90
  export interface WorkspaceVersion {
88
91
  /** The tenant Gitea SHA the version serves — the content identity. */
89
92
  versionId: string;
93
+ /**
94
+ * The merchant-facing monotonic version number ("v42") — derived from first
95
+ * promotion order (see `versionNumber.ts`), shared with the "Live version"
96
+ * metric. The human label; the SHA is demoted to a technical disclosure.
97
+ */
98
+ number: number | null;
90
99
  /** How the version was published (a Gitea commit, or a content hash). */
91
100
  source: "commit" | "hash" | null;
92
101
  /** ISO time the version was PROMOTED onto `live` (the promotion event). */
@@ -133,7 +142,7 @@ export type RollbackMode = "control-plane" | "pointer";
133
142
  /** The versions-panel view model. */
134
143
  export interface VersionsView {
135
144
  /** The current live version + (static) the digest `live/` actually serves. */
136
- current: { versionId: string | null; digest: string | null };
145
+ current: { versionId: string | null; number: number | null; digest: string | null };
137
146
  /** Promotion history, newest first, current marked. */
138
147
  versions: WorkspaceVersion[];
139
148
  /** Whether the tenant publishes its public site to an external static target. */
@@ -189,12 +198,18 @@ export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
189
198
  staticPublish,
190
199
  } = input;
191
200
 
201
+ // The merchant-facing "v42" counter, derived from first-promotion order — the
202
+ // SAME derivation `resolveLiveVersion` feeds the "Live version" metric, so the
203
+ // panel and the metric always agree on a version's number.
204
+ const numbers = assignVersionNumbers(promotions);
205
+
192
206
  const versions: WorkspaceVersion[] = promotions.map((p) => {
193
207
  const facts = manifests.get(p.versionId);
194
208
  const release = releases?.get(p.versionId);
195
209
  const isCurrent = currentVersionId != null && p.versionId === currentVersionId;
196
210
  return {
197
211
  versionId: p.versionId,
212
+ number: numbers.get(p.versionId) ?? null,
198
213
  source: facts?.source ?? null,
199
214
  promotedAt: p.at,
200
215
  createdAt: facts?.createdAt ?? null,
@@ -210,7 +225,11 @@ export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
210
225
  });
211
226
 
212
227
  return {
213
- current: { versionId: currentVersionId, digest: staticPublish ? currentDigest : null },
228
+ current: {
229
+ versionId: currentVersionId,
230
+ number: currentVersionId != null ? numbers.get(currentVersionId) ?? null : null,
231
+ digest: staticPublish ? currentDigest : null,
232
+ },
214
233
  versions,
215
234
  staticPublish,
216
235
  rollbackMode: staticPublish ? "pointer" : "control-plane",