@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,310 @@
1
+ /**
2
+ * Extension-policy compliance obligations — the complete, extensible floor model.
3
+ *
4
+ * An OBLIGATION is a mandated compliance duty: an age/identity gate, excise-tax
5
+ * collection, an FDA nicotine warning, a PACT Act shipment notice, an adult
6
+ * signature, a California Prop 65 warning, a shipping/jurisdiction restriction, and
7
+ * any future duty a later card registers. The set a tenant owes is SERVER-DERIVED
8
+ * from its authoritative `TenantConfig.compliance` — an app manifest or a raw-HTML
9
+ * page can never be an input to resolution, so nothing external can drop or weaken
10
+ * a configured duty (ADR 0014 G1 `integration-regulated-floor-nonwaivable`, D-R7
11
+ * tamper-recovery). Raw HTML and managed apps resolve the SAME obligation set
12
+ * (ADR 0015 `integration-schema-shared`).
13
+ *
14
+ * The compliance-authority axis is kernel-01's `ASSURANCE_SURFACES`
15
+ * (`unregulated-marketing` | `commerce` | `regulated`) — the single vocabulary;
16
+ * this module never invents a second. (The finer `SURFACES` in
17
+ * `integration-capabilities.ts` is a separate axis; ext-policy-11 reconciles the
18
+ * two.)
19
+ *
20
+ * The registry is OPEN — a future duty registers a spec without editing resolution
21
+ * or gating logic. An obligation id OUTSIDE the registry is UNKNOWN and FAILS
22
+ * CLOSED (`verifyObligationCoverage`), never assumed satisfied.
23
+ *
24
+ * Obligation ids `age_gate`, `excise`, and `jurisdiction` are IDENTICAL to the
25
+ * promotion-evidence check ids (ext-prereq-03 `REQUIRED_FOR_PROMOTE`), so
26
+ * ext-policy-06 maps an obligation straight onto its evidence row with no
27
+ * translation table.
28
+ */
29
+ import type { TenantConfig } from "./tenant.js";
30
+ import { ASSURANCE_SURFACES, type AssuranceSurface } from "./integration/assurance.js";
31
+
32
+ /** Contract token — bump the minor for additive vocabulary, the major for a break (ADR 0006 semver). */
33
+ export const COMPLIANCE_OBLIGATION_MODEL = "compliance-obligation-model@1" as const;
34
+ export type ComplianceObligationModel = typeof COMPLIANCE_OBLIGATION_MODEL;
35
+
36
+ /** The only input to obligation resolution: the tenant's authoritative config. */
37
+ export type ObligationTenant = Pick<TenantConfig, "compliance" | "siteType">;
38
+
39
+ /**
40
+ * Platform-owned protected UI/preflight anchors. A third-party integration can
41
+ * never remove, hide, overlay, or mutate these (D-R7). This is the authoritative
42
+ * list the capability-specific conformance suite and ext-policy-09 sentinels key
43
+ * on; here it is the declarative floor, not the detective control.
44
+ */
45
+ export const PROTECTED_SURFACES = [
46
+ "age-gate",
47
+ "compliance-warning",
48
+ "shipping-jurisdiction-notice",
49
+ "cart-preflight",
50
+ "checkout-controls",
51
+ ] as const;
52
+ export type ProtectedSurface = (typeof PROTECTED_SURFACES)[number];
53
+
54
+ /** A registered compliance duty. */
55
+ export interface ObligationSpec {
56
+ /** Stable, wire-serialized obligation id. */
57
+ id: string;
58
+ /** Operator-facing label. */
59
+ label: string;
60
+ /** The compliance-authority surface this duty belongs to (kernel-01 axis). */
61
+ surface: AssuranceSurface;
62
+ /**
63
+ * Platform-owned protected anchors this obligation renders. Non-empty ⇒ the
64
+ * obligation is tamper-protected: an integration can never disable it (D-R7).
65
+ */
66
+ protectedSurfaces: readonly ProtectedSurface[];
67
+ /**
68
+ * Pure predicate over authoritative tenant config: does this tenant configure
69
+ * this obligation? The ONLY resolution input — never an app/raw-HTML declaration.
70
+ */
71
+ applies: (tenant: ObligationTenant) => boolean;
72
+ }
73
+
74
+ /** A resolved (applicable) obligation for a specific tenant. */
75
+ export interface ResolvedObligation {
76
+ id: string;
77
+ label: string;
78
+ surface: AssuranceSurface;
79
+ protectedSurfaces: readonly ProtectedSurface[];
80
+ }
81
+
82
+ const REGISTRY = new Map<string, ObligationSpec>();
83
+
84
+ /**
85
+ * Register a compliance obligation. Extensible seam — a future duty registers a
86
+ * spec here (or from a downstream card) and flows through resolution, surface
87
+ * derivation, coverage verification, and the floor guard with no core edit. A
88
+ * duplicate id is a programming error and throws.
89
+ */
90
+ export function registerObligation(spec: ObligationSpec): void {
91
+ if (REGISTRY.has(spec.id)) {
92
+ throw new Error(`Duplicate compliance obligation id: ${spec.id}`);
93
+ }
94
+ REGISTRY.set(spec.id, spec);
95
+ }
96
+
97
+ /** Every registered obligation spec, in registration order. */
98
+ export function obligationSpecs(): ObligationSpec[] {
99
+ return [...REGISTRY.values()];
100
+ }
101
+
102
+ /** Is this id a registered obligation? Anything else is UNKNOWN → fails closed. */
103
+ export function isKnownObligation(id: string): boolean {
104
+ return REGISTRY.has(id);
105
+ }
106
+
107
+ // ── Built-in floor obligations ──────────────────────────────────────────────
108
+ // Each derives PURELY from `TenantConfig.compliance`. `age_gate`, `excise`, and
109
+ // `jurisdiction` share the promotion-evidence check ids (ext-prereq-03).
110
+
111
+ const BUILTIN_OBLIGATIONS: readonly ObligationSpec[] = [
112
+ {
113
+ id: "age_gate",
114
+ label: "Age/identity verification gate",
115
+ surface: "regulated",
116
+ protectedSurfaces: ["age-gate", "cart-preflight"],
117
+ applies: (t) => !!t.compliance?.minAge,
118
+ },
119
+ {
120
+ id: "excise",
121
+ label: "Excise tax collection",
122
+ surface: "regulated",
123
+ protectedSurfaces: ["checkout-controls"],
124
+ applies: (t) => !!t.compliance?.nicotineWarning || !!t.compliance?.exciseTax,
125
+ },
126
+ {
127
+ id: "nicotine_warning",
128
+ label: "FDA nicotine warning",
129
+ surface: "regulated",
130
+ protectedSurfaces: ["compliance-warning"],
131
+ applies: (t) => !!t.compliance?.nicotineWarning,
132
+ },
133
+ {
134
+ id: "pact_act",
135
+ label: "PACT Act shipment notice",
136
+ surface: "regulated",
137
+ protectedSurfaces: ["compliance-warning", "shipping-jurisdiction-notice"],
138
+ applies: (t) => !!t.compliance?.pactAct,
139
+ },
140
+ {
141
+ id: "adult_signature",
142
+ label: "Adult signature on delivery",
143
+ surface: "regulated",
144
+ protectedSurfaces: ["shipping-jurisdiction-notice", "cart-preflight"],
145
+ applies: (t) => !!t.compliance?.adultSignature,
146
+ },
147
+ {
148
+ id: "prop65",
149
+ label: "California Prop 65 warning",
150
+ surface: "commerce",
151
+ protectedSurfaces: ["compliance-warning"],
152
+ applies: (t) => !!t.compliance?.prop65,
153
+ },
154
+ {
155
+ id: "jurisdiction",
156
+ label: "Restricted shipping / jurisdiction eligibility",
157
+ surface: "commerce",
158
+ protectedSurfaces: ["shipping-jurisdiction-notice", "cart-preflight"],
159
+ applies: (t) =>
160
+ !!t.compliance?.stateEligibility || !!t.compliance?.shippingRestriction,
161
+ },
162
+ ];
163
+
164
+ for (const spec of BUILTIN_OBLIGATIONS) registerObligation(spec);
165
+
166
+ // ── Resolution (server-derived, floor-only) ─────────────────────────────────
167
+
168
+ /**
169
+ * The tenant's REQUIRED obligation set, derived purely from authoritative config.
170
+ * An app/raw-HTML declaration is never an argument, so no integration can drop a
171
+ * duty (ADR G1 / D-R7). Order is stable (registration order) so the serialized
172
+ * `configuredObligations` and the assurance record stay deterministic.
173
+ */
174
+ export function resolveObligations(tenant: ObligationTenant): ResolvedObligation[] {
175
+ const out: ResolvedObligation[] = [];
176
+ for (const spec of REGISTRY.values()) {
177
+ if (spec.applies(tenant)) {
178
+ out.push({
179
+ id: spec.id,
180
+ label: spec.label,
181
+ surface: spec.surface,
182
+ protectedSurfaces: spec.protectedSurfaces,
183
+ });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ /** The resolved obligation ids — the shape gates and the assurance record carry. */
190
+ export function resolveObligationIds(tenant: ObligationTenant): string[] {
191
+ return resolveObligations(tenant).map((o) => o.id);
192
+ }
193
+
194
+ const SURFACE_RANK: Record<AssuranceSurface, number> = Object.fromEntries(
195
+ ASSURANCE_SURFACES.map((s, i) => [s, i]),
196
+ ) as Record<AssuranceSurface, number>;
197
+
198
+ /**
199
+ * The tenant's compliance-authority surface (kernel-01 axis): the MOST restrictive
200
+ * surface any applied obligation implies, else the siteType floor (a marketing site
201
+ * with no duty is `unregulated-marketing`; a commerce site is `commerce`).
202
+ */
203
+ export function resolveObligationSurface(tenant: ObligationTenant): AssuranceSurface {
204
+ let surface: AssuranceSurface =
205
+ tenant.siteType === "marketing" ? "unregulated-marketing" : "commerce";
206
+ for (const o of resolveObligations(tenant)) {
207
+ if (SURFACE_RANK[o.surface] > SURFACE_RANK[surface]) surface = o.surface;
208
+ }
209
+ return surface;
210
+ }
211
+
212
+ /** True iff the tenant resolves to the `regulated` authority surface. */
213
+ export function isRegulated(tenant: ObligationTenant): boolean {
214
+ return resolveObligationSurface(tenant) === "regulated";
215
+ }
216
+
217
+ /**
218
+ * The union of platform-owned protected anchors the tenant's applied obligations
219
+ * render, in canonical order. Drives the conformance suite and the ext-policy-09
220
+ * sentinels — the set an integration can never disable (D-R7).
221
+ */
222
+ export function protectedSurfacesFor(tenant: ObligationTenant): ProtectedSurface[] {
223
+ const seen = new Set<ProtectedSurface>();
224
+ for (const o of resolveObligations(tenant)) {
225
+ for (const s of o.protectedSurfaces) seen.add(s);
226
+ }
227
+ return PROTECTED_SURFACES.filter((s) => seen.has(s));
228
+ }
229
+
230
+ // ── Fail-closed coverage gate (mirrors ext-prereq-03 verifyPromotionEvidence) ──
231
+
232
+ export interface ObligationCoverageVerdict {
233
+ /** True only when the declaration COMPLETELY, non-ambiguously covers the floor. */
234
+ ok: boolean;
235
+ /** Every reason coverage is insufficient (empty ⇔ ok). */
236
+ blockers: string[];
237
+ }
238
+
239
+ /**
240
+ * The single fail-CLOSED authority for "does this declaration satisfy the tenant's
241
+ * compliance floor?". The required set is SERVER-DERIVED from tenant config, never
242
+ * taken from the (spoofable) declaration. Rejects on any of: an absent declaration
243
+ * (no array), an omitted required obligation, an UNKNOWN obligation id (registry
244
+ * drift / a spoofed row), or a duplicated id (an ambiguous set that could mask a
245
+ * missing duty). Shared by the raw-HTML path and the managed-app path (ADR 0015).
246
+ */
247
+ export function verifyObligationCoverage(
248
+ tenant: ObligationTenant,
249
+ declaredIds: readonly string[] | null | undefined,
250
+ ): ObligationCoverageVerdict {
251
+ // Absent declaration — fail closed. Nothing declared against a regulated floor is
252
+ // exactly what this gate exists to refuse.
253
+ if (!Array.isArray(declaredIds)) {
254
+ return {
255
+ ok: false,
256
+ blockers: [
257
+ "Obligation coverage is absent — nothing declared against the compliance floor.",
258
+ ],
259
+ };
260
+ }
261
+
262
+ const blockers: string[] = [];
263
+ const seen = new Set<string>();
264
+ for (const id of declaredIds) {
265
+ if (!isKnownObligation(id)) {
266
+ blockers.push(`Unknown compliance obligation declared: ${id}`);
267
+ }
268
+ if (seen.has(id)) {
269
+ blockers.push(`Duplicated compliance obligation declared: ${id}`);
270
+ }
271
+ seen.add(id);
272
+ }
273
+
274
+ for (const id of resolveObligationIds(tenant)) {
275
+ if (!seen.has(id)) {
276
+ blockers.push(`Required compliance obligation omitted: ${id}`);
277
+ }
278
+ }
279
+
280
+ return { ok: blockers.length === 0, blockers };
281
+ }
282
+
283
+ export interface ObligationFloorViolation {
284
+ obligation: string;
285
+ reason: string;
286
+ }
287
+
288
+ /**
289
+ * Surface any attempt by an integration to disable a configured obligation. The
290
+ * resolved floor is unchanged regardless — `resolveObligations` never reads this
291
+ * input — so this only records the attempt for denial/audit (D-R7). Any id in
292
+ * `requestedDisable` that names a resolved required obligation is a violation.
293
+ */
294
+ export function obligationFloorViolations(
295
+ tenant: ObligationTenant,
296
+ requestedDisable?: readonly string[] | null,
297
+ ): ObligationFloorViolation[] {
298
+ if (!requestedDisable) return [];
299
+ const required = new Set(resolveObligationIds(tenant));
300
+ const out: ObligationFloorViolation[] = [];
301
+ for (const id of requestedDisable) {
302
+ if (required.has(id)) {
303
+ out.push({
304
+ obligation: id,
305
+ reason: `${id} is a configured compliance obligation and cannot be disabled by an integration.`,
306
+ });
307
+ }
308
+ }
309
+ return out;
310
+ }
@@ -10,6 +10,11 @@
10
10
  * The middleware decides the header name — `Content-Security-Policy` (enforce)
11
11
  * vs `Content-Security-Policy-Report-Only` (rollout). This only builds the value.
12
12
  */
13
+ // The embed catalog DATA lives in a dependency-free .mjs so plain-node tooling
14
+ // (the dev-loop lint) can share this exact copy — see embed-catalog.mjs. csp.ts
15
+ // owns the TYPE (EmbedProvider) and re-exports the data under it below.
16
+ import { EMBED_PROVIDERS as EMBED_PROVIDERS_DATA } from "./embed-catalog.mjs";
17
+
13
18
  export interface CspOptions {
14
19
  /** Per-request nonce; the same value goes on the platform's inline tags. */
15
20
  nonce: string;
@@ -85,6 +90,100 @@ export const STOREFRONT_CSP_ALLOWLIST = {
85
90
  connectSrc: ["https://cloudflareinsights.com"],
86
91
  } as const;
87
92
 
93
+ /**
94
+ * Curated catalog of known third-party embeds a tenant may opt into. Keyed by a
95
+ * stable provider slug the tenant lists in `.tot/config.json` `embeds:`; the
96
+ * platform (never the tenant) owns the exact origins each provider needs, so a
97
+ * tenant can enable "pipedrive" without ever hand-writing a CSP origin — and a
98
+ * novel provider requires a reviewed catalog entry here, not a config edit.
99
+ *
100
+ * `markers` are substrings that appear in rendered HTML when the embed is
101
+ * actually present on a page (so `embedCspAdditions` can add origins ONLY to the
102
+ * pages that use them — the same derive-from-actual-usage posture as
103
+ * {@link appWidgetCspAdditions}). Origins are EXACT hosts, never wildcards. An
104
+ * embed renders in its own cross-origin iframe governed by the provider's CSP,
105
+ * so `frameSrc` mounts it and `connectSrc`/`scriptSrc` cover only what the
106
+ * loader needs in the PARENT document.
107
+ */
108
+ export interface EmbedProvider {
109
+ /** Human label for lint/report messages. */
110
+ label: string;
111
+ /** Substrings whose presence in rendered HTML means the embed is on the page. */
112
+ markers: readonly string[];
113
+ scriptSrc?: readonly string[];
114
+ connectSrc?: readonly string[];
115
+ frameSrc?: readonly string[];
116
+ }
117
+
118
+ export const EMBED_PROVIDERS: Readonly<Record<string, EmbedProvider>> =
119
+ EMBED_PROVIDERS_DATA;
120
+
121
+ export type EmbedProviderKey = keyof typeof EMBED_PROVIDERS;
122
+
123
+ /**
124
+ * Canonical published-artifact path for a tenant's embed opt-in — the SAME
125
+ * colocated `tenants/<id>/…` config-as-code spine as capabilities/theme. A
126
+ * tenant self-serves by committing `embeds.json` (a JSON array of catalog
127
+ * slugs, e.g. `["pipedrive"]`) to their repo; the reconcile pipeline keys the
128
+ * KV artifact by this path and the storefront reads it back per environment
129
+ * (localhost=working tree, preview=PR head, live=promoted). No platform code
130
+ * change needed to enable a catalogued embed.
131
+ */
132
+ export function embedsArtifactPath(tenantId: string): string {
133
+ return `tenants/${tenantId}/embeds.json`;
134
+ }
135
+
136
+ /**
137
+ * Effective embed opt-in for a tenant: the build-time `TenantConfig.embeds`
138
+ * default overlaid with the tenant's published `embeds.json` (self-serve,
139
+ * per-environment) when present — deduped and FILTERED to slugs that exist in
140
+ * EMBED_PROVIDERS. The filter is the security floor: a tenant can only enable a
141
+ * platform-vetted provider, never introduce an arbitrary origin; an unknown
142
+ * slug is dropped here (and flagged by the embeds.json validator + lint).
143
+ */
144
+ export function resolveEffectiveEmbeds(
145
+ buildDefault: readonly string[] | undefined,
146
+ published: readonly string[] | null,
147
+ ): string[] {
148
+ const merged = new Set<string>([...(buildDefault ?? []), ...(published ?? [])]);
149
+ return [...merged].filter((slug) => slug in EMBED_PROVIDERS);
150
+ }
151
+
152
+ export interface EmbedCspAdditions {
153
+ scriptSrc: string[];
154
+ connectSrc: string[];
155
+ frameSrc: string[];
156
+ }
157
+
158
+ /**
159
+ * CSP additions for the third-party embeds ACTUALLY rendered on this page.
160
+ *
161
+ * An origin is added only when BOTH hold: (1) the tenant opted the provider in
162
+ * via `.tot/config.json` `embeds:` (audit trail + platform review of novel
163
+ * providers), and (2) the provider's marker appears in the rendered HTML (so a
164
+ * page that doesn't embed the form never carries the origin). This keeps the
165
+ * strict policy tenant- AND page-scoped — never a blanket global relaxation.
166
+ *
167
+ * Unknown keys in `enabled` are ignored here (the lint surfaces them); a marker
168
+ * present WITHOUT the matching opt-in is likewise not allowed here — that's the
169
+ * blocked-embed case the lint/report loop is meant to catch before it ships.
170
+ */
171
+ export function embedCspAdditions(
172
+ enabled: readonly string[],
173
+ renderedHtml: string,
174
+ ): EmbedCspAdditions {
175
+ const out: EmbedCspAdditions = { scriptSrc: [], connectSrc: [], frameSrc: [] };
176
+ for (const key of new Set(enabled)) {
177
+ const provider = EMBED_PROVIDERS[key];
178
+ if (!provider) continue;
179
+ if (!provider.markers.some((m) => renderedHtml.includes(m))) continue;
180
+ if (provider.scriptSrc) out.scriptSrc.push(...provider.scriptSrc);
181
+ if (provider.connectSrc) out.connectSrc.push(...provider.connectSrc);
182
+ if (provider.frameSrc) out.frameSrc.push(...provider.frameSrc);
183
+ }
184
+ return out;
185
+ }
186
+
88
187
  /**
89
188
  * CSP additions for a managed-checkout tenant. The loader comes from the checkout
90
189
  * CDN; cart requests, frames, and add-to-cart form POSTs hit the store's own
@@ -142,6 +142,8 @@ export interface EnvResolutionInput {
142
142
  /** Platform preview-signing secret (env), if configured. */
143
143
  secret?: string | null;
144
144
  nowSeconds: number;
145
+ /** Request pathname — used only to exempt the token dispenser itself (see {@link isPreviewDispatcherPath}). */
146
+ pathname?: string;
145
147
  }
146
148
 
147
149
  export interface EnvResolution {
@@ -152,16 +154,37 @@ export interface EnvResolution {
152
154
  reject?: { status: number; message: string };
153
155
  }
154
156
 
157
+ /**
158
+ * The bare tenant-preview DISPATCHER path (`/<tenant>/preview` or
159
+ * `/<tenant>/preview/`, e.g. `pages/preview/index.astro`, tenant-first) — the page
160
+ * whose ENTIRE job is to mint a fresh `__preview` token and hand it to the
161
+ * visitor. On a preview-prefixed host this path would otherwise hit the SAME
162
+ * "no token -> 401" gate `resolveCustomizationEnv` enforces for every other
163
+ * path, deadlocking a signed-in-but-token-less visitor: they can never reach
164
+ * the one page that gives them a token. Distinct from `isCandidatePreviewPath`
165
+ * (loginGate.ts) — that exempts several preview routes from the LOGIN wall; this
166
+ * exempts exactly ONE route from the TOKEN wall. `<tenant>` is required to look
167
+ * like a real tenant id (contains a `.`) so this never accidentally matches a
168
+ * sibling route. Matched on the pre-strip request path.
169
+ */
170
+ export function isPreviewDispatcherPath(pathname: string): boolean {
171
+ const m = pathname.match(/^\/([^/]+)\/preview\/?$/);
172
+ return m != null && m[1]!.includes(".");
173
+ }
174
+
155
175
  /**
156
176
  * Decide which customization environment to serve. Production hosts always get
157
177
  * `live` and never consult a token. Preview hosts are auth-gated: a valid,
158
178
  * unexpired, tenant-matching token yields `test` (noindex); anything else is
159
- * rejected.
179
+ * rejected. The one exception is the token-dispenser path itself (see
180
+ * {@link isPreviewDispatcherPath}) — it renders the dispatcher's own minimal
181
+ * page, never tenant content, so it's let through as `live`/no-token rather
182
+ * than 401ing on the very request that would otherwise fetch it a token.
160
183
  */
161
184
  export async function resolveCustomizationEnv(
162
185
  input: EnvResolutionInput,
163
186
  ): Promise<EnvResolution> {
164
- if (!input.isPreviewHost) {
187
+ if (!input.isPreviewHost || isPreviewDispatcherPath(input.pathname ?? "")) {
165
188
  return { env: "live", noindex: false };
166
189
  }
167
190
  if (!input.secret) {
@@ -15,6 +15,7 @@ import {
15
15
  type ChannelName,
16
16
  type PointerRecord,
17
17
  type SiteVersionManifest,
18
+ type SiteVersionSummary,
18
19
  } from "./customization-versioning.js";
19
20
  import type { CustomizationEnv } from "./customization-preview.js";
20
21
 
@@ -93,6 +94,27 @@ export async function resolveChannelVersion(
93
94
  return pointer?.channels?.[channel] ?? null;
94
95
  }
95
96
 
97
+ /**
98
+ * The static-bundle `treeDigest` a channel currently serves (ADR 0012), or null
99
+ * when the tenant has no digest companion for that channel (a hosted tenant — it
100
+ * sets only `channels`, never `channelDigests`).
101
+ *
102
+ * This is the RECONCILED read the digest-addressed serving path
103
+ * (`static-bundle.ts`), the pointer verify (`createPointerVerifyTarget`), and
104
+ * the pointer-served "current" read (`resolveStaticLiveDigestFromPointer`) use
105
+ * INSTEAD of `resolveChannelVersion` for a static tenant: the versionId
106
+ * (`channels[ch]`, the Gitea sha) is NOT the R2 key — the treeDigest is, and it
107
+ * lives here. Reads the pointer fresh; never throws.
108
+ */
109
+ export async function resolveChannelDigest(
110
+ get: KvGet,
111
+ tenantId: string,
112
+ channel: ChannelName,
113
+ ): Promise<string | null> {
114
+ const pointer = await readPointer(get, tenantId);
115
+ return pointer?.channelDigests?.[channel] ?? null;
116
+ }
117
+
96
118
  /** Read one immutable site-version manifest, or null. Never throws. */
97
119
  export async function readSiteVersion(
98
120
  get: KvGet,
@@ -108,12 +130,112 @@ export async function readSiteVersion(
108
130
  }
109
131
  }
110
132
 
133
+ /** A manifest is BUILT (servable) when it exists AND materialized ≥1 artifact. */
134
+ function isBuiltManifest(m: SiteVersionManifest | null): m is SiteVersionManifest {
135
+ return !!m && Object.keys(m.artifacts ?? {}).length > 0;
136
+ }
137
+
138
+ /**
139
+ * Read a tenant's version-history index (list of summaries), NEWEST first — the
140
+ * runtime read-side view of the same `siteVersionIndexKey` the KV store appends
141
+ * to (stored oldest-first). Never throws — malformed/absent → `[]`.
142
+ */
143
+ export async function readSiteVersionIndex(
144
+ get: KvGet,
145
+ tenantId: string,
146
+ ): Promise<SiteVersionSummary[]> {
147
+ const raw = await get(siteVersionIndexKey(tenantId));
148
+ if (!raw) return [];
149
+ try {
150
+ return (JSON.parse(raw) as SiteVersionSummary[]).slice().reverse();
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ /** The outcome of self-healing a channel pointer to a SERVABLE version. */
157
+ export interface ServableChannelVersion {
158
+ /**
159
+ * The versionId to actually SERVE from: the pointed version when it is built,
160
+ * else the newest BUILT version in history (self-heal), else `null` (nothing
161
+ * built at all — the caller falls back to build-time, still never a bare 404).
162
+ */
163
+ versionId: string | null;
164
+ /** The versionId the channel pointer NAMES (may dangle at an unbuilt version). */
165
+ pointedVersionId: string | null;
166
+ /**
167
+ * True when the pointer named a version with no built manifest — so the read
168
+ * was self-healed onto an older built version (or found none). Signals the
169
+ * caller to enqueue a rebuild of the pointed version.
170
+ */
171
+ dangling: boolean;
172
+ }
173
+
174
+ /** Optional seam so a self-healing read can enqueue a rebuild of the dangling
175
+ * version. Best-effort/fail-open: never awaited into the serve path, never
176
+ * throws through it. Absent ⇒ pure resolution (no side effect). */
177
+ export type EnqueueRebuild = (info: {
178
+ tenantId: string;
179
+ channel: ChannelName;
180
+ versionId: string;
181
+ }) => void;
182
+
183
+ /**
184
+ * Resolve the SERVABLE version for a channel, self-healing a dangling pointer.
185
+ *
186
+ * A channel pointer can name a version that was never built (or whose manifest
187
+ * was evicted). Serving that naively empties/404s the whole surface. Instead: if
188
+ * the pointed version has no built manifest, fall back to the newest version in
189
+ * history that DOES, and flag `dangling` so the caller can enqueue a rebuild.
190
+ * Only kicks in when the pointed
191
+ * manifest is entirely absent/unbuilt — a present manifest that merely omits one
192
+ * path stays authoritative (unchanged behavior). Reads fresh; never throws.
193
+ */
194
+ export async function resolveServableChannelVersion(
195
+ get: KvGet,
196
+ tenantId: string,
197
+ channel: ChannelName,
198
+ enqueueRebuild?: EnqueueRebuild,
199
+ ): Promise<ServableChannelVersion> {
200
+ const pointedVersionId = await resolveChannelVersion(get, tenantId, channel);
201
+ if (!pointedVersionId) {
202
+ return { versionId: null, pointedVersionId: null, dangling: false };
203
+ }
204
+ const pointed = await readSiteVersion(get, tenantId, pointedVersionId);
205
+ if (isBuiltManifest(pointed)) {
206
+ return { versionId: pointedVersionId, pointedVersionId, dangling: false };
207
+ }
208
+ // Dangling: the pointer names an unbuilt/absent version. Self-heal onto the
209
+ // newest BUILT version in history so the surface keeps rendering last-good.
210
+ let healed: string | null = null;
211
+ for (const summary of await readSiteVersionIndex(get, tenantId)) {
212
+ if (summary.versionId === pointedVersionId) continue;
213
+ if (isBuiltManifest(await readSiteVersion(get, tenantId, summary.versionId))) {
214
+ healed = summary.versionId;
215
+ break;
216
+ }
217
+ }
218
+ if (enqueueRebuild) {
219
+ try {
220
+ enqueueRebuild({ tenantId, channel, versionId: pointedVersionId });
221
+ } catch {
222
+ /* fail-open: enqueue is best-effort, never breaks the read */
223
+ }
224
+ }
225
+ return { versionId: healed, pointedVersionId, dangling: true };
226
+ }
227
+
111
228
  /**
112
229
  * Resolve a single artifact's content via the VERSIONED path:
113
230
  * channel → versionId → site-version manifest → content hash → bytes. Returns
114
231
  * null (so the caller falls back to build-time) when the tenant has no channel
115
- * pointer, the version manifest is missing, the path isn't in the snapshot, or
116
- * the content is gone. Never throws.
232
+ * pointer, the path isn't in the snapshot, or the content is gone. Never throws.
233
+ *
234
+ * Self-healing: when the channel pointer DANGLES (names an unbuilt/absent
235
+ * version), the artifact is read from the newest built version instead of
236
+ * returning null for every path — so a dangling `preview`/`live` pointer serves
237
+ * the last built site rather than 404ing the whole surface. See
238
+ * {@link resolveServableChannelVersion}.
117
239
  */
118
240
  export async function readVersionedArtifact(
119
241
  get: KvGet,
@@ -121,7 +243,7 @@ export async function readVersionedArtifact(
121
243
  channel: ChannelName,
122
244
  path: string,
123
245
  ): Promise<string | null> {
124
- const versionId = await resolveChannelVersion(get, tenantId, channel);
246
+ const { versionId } = await resolveServableChannelVersion(get, tenantId, channel);
125
247
  if (!versionId) return null;
126
248
  const manifest = await readSiteVersion(get, tenantId, versionId);
127
249
  const contentHash = manifest?.artifacts?.[path];
@@ -163,3 +285,34 @@ export async function readPublishedArtifact(
163
285
  if (!versionId) return null;
164
286
  return get(customizationArtifactKey(tenantId, versionId));
165
287
  }
288
+
289
+ /**
290
+ * List artifact paths in the immutable site version selected for `(tenant, env)`.
291
+ *
292
+ * `null` means the tenant/channel has not adopted versioned publishing, so a
293
+ * caller may use its local/legacy fallback. Once a channel pointer exists the
294
+ * manifest is authoritative and this returns an array (possibly empty). This is
295
+ * the enumeration companion to `readPublishedArtifact`: indexes, sitemaps, and
296
+ * collection routes can discover one tenant's content at request time without
297
+ * bundling every configured tenant into the framework Worker.
298
+ */
299
+ export async function listPublishedArtifactPaths(
300
+ get: KvGet,
301
+ tenantId: string,
302
+ env: CustomizationEnv,
303
+ prefix = "",
304
+ ): Promise<string[] | null> {
305
+ const pointer = await readPointer(get, tenantId);
306
+ if (!pointer) return null;
307
+
308
+ const channel = channelFromEnv(env);
309
+ if (!pointer.channels?.[channel]) return null;
310
+
311
+ const { versionId } = await resolveServableChannelVersion(get, tenantId, channel);
312
+ if (!versionId) return [];
313
+ const manifest = await readSiteVersion(get, tenantId, versionId);
314
+ if (!manifest) return [];
315
+ return Object.keys(manifest.artifacts ?? {})
316
+ .filter((path) => path.startsWith(prefix))
317
+ .sort((a, b) => a.localeCompare(b));
318
+ }
@@ -37,6 +37,15 @@ export type ChannelPointers = Partial<Record<ChannelName, string>>;
37
37
  * - `channels`: the versioned-publish channel pointers (`live`/`preview` →
38
38
  * a single site `versionId`). Additive + optional; when present it is
39
39
  * authoritative for that channel and shadows the legacy selection.
40
+ * - `channelDigests`: (ADR 0012) the RECONCILED static-serving companion to
41
+ * `channels`. For a static-publish tenant, `channels[ch]` still holds the site
42
+ * `versionId` (the tenant Gitea commit sha — the hosted contract), while
43
+ * `channelDigests[ch]` holds that same publish's bundle `treeDigest` (the
44
+ * content-addressed R2 key prefix `versions/<digest>/` `static-bundle.ts`
45
+ * reads). The two are DISTINCT namespaces — the Gitea sha is NOT the treeDigest
46
+ * — and are written in the SAME CAS pointer write (`setChannel`), so they can
47
+ * never drift. Absent for a hosted tenant (which only ever sets/reads
48
+ * `channels`).
40
49
  * - `rev`: optimistic-concurrency token, incremented on every successful write.
41
50
  */
42
51
  export interface PointerRecord {
@@ -45,6 +54,7 @@ export interface PointerRecord {
45
54
  live: Selection;
46
55
  test: Selection;
47
56
  channels?: ChannelPointers;
57
+ channelDigests?: ChannelPointers;
48
58
  }
49
59
 
50
60
  export function emptyPointer(tenantId: string): PointerRecord {