@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
@@ -55,7 +55,7 @@ function localTenantDirIds(): string[] {
55
55
  return [...ids];
56
56
  }
57
57
 
58
- // (E2) Catalog fixtures committed on disk, keyed by data scope (== out/catalog dir
58
+ // Catalog fixtures committed on disk, keyed by data scope (== out/catalog dir
59
59
  // name). A dev tenant whose scope has fixtures is served as a real COMMERCE tenant
60
60
  // (see devTenantSeed); one without them degrades to a marketing home. Keys only —
61
61
  // the same fixtures the FixtureToTClient reads. Repo root is four up from config/.
@@ -73,7 +73,7 @@ function catalogScopes(): Set<string> {
73
73
  return scopes;
74
74
  }
75
75
 
76
- // (E3) A slug tenant (dir name != its domain) declares its resolvable appDomain in
76
+ // A slug tenant (dir name != its domain) declares its resolvable appDomain in
77
77
  // the checkout's `.tot/config.json` `scope`. Read eagerly (tiny JSON) so synthesis
78
78
  // can resolve it at `/<domain>/`; trusted only when it looks like a hostname (has a
79
79
  // dot), mirroring scripts/tot-dev.mjs resolveDomain. Only the DEV fallback consults
@@ -110,8 +110,8 @@ function slugScopes(): Map<string, string> {
110
110
  function devFallbackResolver(): TenantResolver {
111
111
  if (!import.meta.env.DEV) return staticResolver;
112
112
  const staticIds = new Set(staticTenants.map((t) => t.tenant_id));
113
- const withCatalog = catalogScopes(); // E2
114
- const scopes = slugScopes(); // E3
113
+ const withCatalog = catalogScopes();
114
+ const scopes = slugScopes();
115
115
  const dev = devTenantsToSeed(localTenantDirIds(), staticIds, getTenantTheme, {
116
116
  scopeFor: (id) => scopes.get(id),
117
117
  hasCatalog: (scope) => withCatalog.has(scope),
@@ -23,6 +23,9 @@ import { referenceTheme } from "@/themes/reference";
23
23
  import { mergeTheme } from "@/themes/schema";
24
24
  import { organizationLd, websiteLd } from "@/lib/jsonld";
25
25
  import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
26
+ import { resolveHeaderVariant, resolveFooterVariant } from "@/lib/chrome/variant";
27
+ import type { HeaderVariant, FooterVariant } from "@/lib/chrome/model";
28
+ import { renderIntegrationPhases } from "@tot/public-runtime";
26
29
  import { readViewerSession } from "@/lib/auth/route";
27
30
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
28
31
  import { getRegistryService } from "@/lib/apps/adminService.js";
@@ -36,15 +39,70 @@ interface Props {
36
39
  ogImage?: string;
37
40
  noindex?: boolean;
38
41
  jsonLd?: Record<string, unknown> | Record<string, unknown>[];
42
+ /** Alternate RSS/Atom feeds this page advertises (threaded to <Seo>). */
43
+ feeds?: { title: string; href: string }[];
39
44
  /** Wider pages (PLP/home) drop the prose max-width. */
40
45
  fullBleed?: boolean;
46
+ /**
47
+ * Explicit per-page header/footer content-variant override (see
48
+ * lib/chrome/model.ts HeaderVariant/FooterVariant). NEVER inferred from the
49
+ * route/CSS/title — a page that wants e.g. a minimal checkout header sets
50
+ * this prop. Absent ⇒ the tenant's chrome-config default
51
+ * (chrome.header.variant / chrome.footer.variant). An unrecognized value
52
+ * (page-supplied or the config's own default) fails closed with an
53
+ * actionable error rather than silently falling back.
54
+ */
55
+ headerVariant?: HeaderVariant;
56
+ footerVariant?: FooterVariant;
57
+ // --- Article SEO hardening: pass-through to <Seo> (all additive) ---
58
+ article?: import("@/lib/seo/meta.js").ArticleMeta;
59
+ imageAlt?: string;
60
+ imageWidth?: number;
61
+ imageHeight?: number;
62
+ twitterSite?: string;
63
+ twitterCreator?: string;
64
+ canonicalOverride?: string;
65
+ // --- hreflang / <html lang> (additive) ---
66
+ /** Per-variant `hreflang` alternates for a translated article; threaded to
67
+ * <Seo>. Absent/empty ⇒ no alternate links (singleton unchanged). */
68
+ alternates?: import("@/lib/seo/alternates.js").Alternate[];
69
+ /** Overrides `<html lang>` (e.g. a translated article's `inLanguage`). Absent ⇒
70
+ * the tenant-locale default, so non-translated pages are unchanged. */
71
+ lang?: string;
41
72
  }
42
73
 
43
- const { title, description, path, ogType, ogImage, noindex, jsonLd } =
44
- Astro.props;
74
+ const {
75
+ title,
76
+ description,
77
+ path,
78
+ ogType,
79
+ ogImage,
80
+ noindex,
81
+ jsonLd,
82
+ feeds,
83
+ article,
84
+ imageAlt,
85
+ imageWidth,
86
+ imageHeight,
87
+ twitterSite,
88
+ twitterCreator,
89
+ canonicalOverride,
90
+ alternates,
91
+ lang,
92
+ headerVariant,
93
+ footerVariant,
94
+ } = Astro.props;
45
95
  const { tenant, themeStyle, canonicalBase, basePath, cspNonce, features, capabilities } =
46
96
  Astro.locals;
47
97
 
98
+ // Fixed-phase integration engine (ext-kernel-04): ONE engine emits the
99
+ // renderer-owned phases (platform.environment, consent.defaults, head.early,
100
+ // body.start, document.deferred) into the head + body-start + body-deferred
101
+ // boundaries. `rawHtmlResponse` is the other mount of the SAME engine, so both
102
+ // documents carry byte-identical phase markup. The nonce is stamped per response
103
+ // from a placeholder by the middleware, never serialized (ADR 0018).
104
+ const integrationPhases = renderIntegrationPhases(Astro.locals.integrationPlan, cspNonce);
105
+
48
106
  // Organization JSON-LD is emitted site-wide (brief §8); pages add their own
49
107
  // graphs (Product, BreadcrumbList…) via the jsonLd prop.
50
108
  const pageGraphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
@@ -58,6 +116,12 @@ const theme = mergeTheme(referenceTheme, tenant.theme_tokens);
58
116
 
59
117
  const chrome = await Astro.locals.content.getChrome();
60
118
 
119
+ // Explicit variant resolution (fail-closed): the page's override wins over
120
+ // the tenant's chrome-config default; an unrecognized value throws with an
121
+ // actionable message rather than silently falling back (lib/chrome/variant.ts).
122
+ const resolvedHeaderVariant = resolveHeaderVariant(headerVariant, chrome.header.variant);
123
+ const resolvedFooterVariant = resolveFooterVariant(footerVariant, chrome.footer.variant);
124
+
61
125
  // SHARED CHROME: if the tenant ships content/<id>/chrome.html (the pixel-perfect
62
126
  // header/footer the raw marketing pages use), render THAT chrome around the
63
127
  // Layout content too — so commerce pages (collections/products/search/…) share
@@ -65,7 +129,7 @@ const chrome = await Astro.locals.content.getChrome();
65
129
  // <!--PAGE_BODY-->: everything before = header, after = footer; pull its tenant
66
130
  // stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
67
131
  // chrome.html keep the token-driven Header/Footer (below).
68
- const chromeHtml = await Astro.locals.content.getChromeHtml();
132
+ const chromeHtml = await Astro.locals.content.getChromeHtml(path);
69
133
  let sharedChrome: { header: string; footer: string; css: string[] } | null = null;
70
134
  if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
71
135
  const headEnd = chromeHtml.indexOf("</head>");
@@ -85,7 +149,7 @@ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
85
149
 
86
150
  const compliance = tenant.compliance;
87
151
 
88
- // Site-chrome data resolution (unit I4 — chrome/* is the canonical, prop-driven
152
+ // Site-chrome data resolution (chrome/* is the canonical, prop-driven
89
153
  // component set; this Layout is the ONE place that resolves session/feature/
90
154
  // theme state into their props. Skipped entirely when sharedChrome (a raw
91
155
  // content/<id>/chrome.html override) is present — that path wins outright.
@@ -119,25 +183,34 @@ if (features.catalog) {
119
183
  // white logo); a light header always shows the text wordmark (no logo image).
120
184
  const headerDark = theme.brand.headerVariant === "dark";
121
185
  const headerLogoSrc = headerDark ? theme.brand.logoLight : theme.brand.logoDark;
122
- const headerLogo =
186
+ const themeHeaderLogo =
123
187
  headerDark && headerLogoSrc && /^(https?:|\/)/.test(headerLogoSrc)
124
188
  ? { src: headerLogoSrc, alt: tenant.displayName }
125
189
  : undefined;
126
190
 
191
+ // chrome.header.brand (lib/chrome/model.ts ChromeBrand) is the governed,
192
+ // schema-validated brand identity — SOURCE OF TRUTH per field when set,
193
+ // falling back to the theme-derived values above only where a field is
194
+ // absent, so a tenant that hasn't authored a brand config is unaffected.
195
+ const brand = chrome.header.brand;
196
+ const brandLogoValid = brand?.logo && /^(https?:|\/)/.test(brand.logo.src) ? brand.logo : undefined;
197
+ const headerLogo = brandLogoValid ?? themeHeaderLogo;
198
+ const headerWordmark = brand?.label ?? tenant.displayName;
199
+ const headerBrandHref = brand?.href;
200
+
127
201
  // Footer bottom bar: a "Legal"-titled column's links win; otherwise every
128
202
  // column's links flatten into the policy-link row (mirrors the retired
129
203
  // nav/Footer.astro exactly).
130
204
  const legalCol = chrome.footer.columns.find((c) => /legal/i.test(c.title));
131
205
  const bottomLinks = legalCol ? legalCol.links : chrome.footer.columns.flatMap((c) => c.links);
132
- const legalLine = `© ${new Date().getFullYear()} ${tenant.displayName}${
133
- chrome.footer.legal ? ` · ${chrome.footer.legal}` : ""
134
- }`;
206
+ const legalBase = chrome.footer.bottom?.copyright ?? `© ${new Date().getFullYear()} ${tenant.displayName}`;
207
+ const legalLine = `${legalBase}${chrome.footer.bottom?.note ? ` · ${chrome.footer.bottom.note}` : ""}`;
135
208
 
136
209
  const analyticsToken = import.meta.env.PUBLIC_CF_ANALYTICS_TOKEN;
137
210
  const analyticsEnabled =
138
211
  analyticsToken && !/REPLACE|PLACEHOLDER/i.test(analyticsToken);
139
212
 
140
- // Private-app widgets (D5) — global.footer slot. Resolved once here so it
213
+ // Private-app widgets — global.footer slot. Resolved once here so it
141
214
  // renders for BOTH footer paths below (the tenant-owned chrome.html splice
142
215
  // and the generic <Footer>) instead of being owned by either. No dark-mode
143
216
  // preference infra exists yet — "light" is the only theme hint until it does.
@@ -156,10 +229,18 @@ const footerWidgets = await renderWidgetSlot(
156
229
  ---
157
230
 
158
231
  <!doctype html>
159
- <html lang={tenant.locale.split("-")[0] ?? "en"} data-base-path={basePath || undefined}>
232
+ <html lang={lang ?? tenant.locale.split("-")[0] ?? "en"} data-base-path={basePath || undefined}>
160
233
  <head>
161
234
  <meta charset="utf-8" />
162
235
  <meta name="viewport" content="width=device-width, initial-scale=1" />
236
+
237
+ {/* Renderer-owned head phases (platform.environment → consent.defaults →
238
+ head.early), emitted before any tenant/app code — the Layout half of "one
239
+ engine, two mounts" (rawHtmlResponse is the other). Same engine output, so
240
+ both mounts are byte-identical. Carries only public values; the nonce is a
241
+ script attribute stamped per response, never serialized (ADR 0018). */}
242
+ <Fragment set:html={integrationPhases.head} />
243
+
163
244
  <link rel="icon" type="image/svg+xml" href={theme.brand.favicon} />
164
245
 
165
246
  {/* Per-tenant theme tokens injected at the edge (brief §7). The id is the
@@ -185,6 +266,15 @@ const footerWidgets = await renderWidgetSlot(
185
266
  ogImage={ogImage}
186
267
  noindex={noindex}
187
268
  jsonLd={allJsonLd}
269
+ feeds={feeds}
270
+ article={article}
271
+ imageAlt={imageAlt}
272
+ imageWidth={imageWidth}
273
+ imageHeight={imageHeight}
274
+ twitterSite={twitterSite}
275
+ twitterCreator={twitterCreator}
276
+ canonicalOverride={canonicalOverride}
277
+ alternates={alternates}
188
278
  />
189
279
 
190
280
  {
@@ -198,6 +288,10 @@ const footerWidgets = await renderWidgetSlot(
198
288
  }
199
289
  </head>
200
290
  <body>
291
+ {/* body.start fallback boundary (ext-kernel-04) — the renderer-owned mount
292
+ point for body-start contributions, before any page content. */}
293
+ <Fragment set:html={integrationPhases.bodyStart} />
294
+
201
295
  <a
202
296
  href="#main"
203
297
  class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-3 focus:rounded-md focus:bg-[var(--color-primary)] focus:px-4 focus:py-2 focus:text-[var(--color-primary-contrast)]"
@@ -214,16 +308,20 @@ const footerWidgets = await renderWidgetSlot(
214
308
  ) : (
215
309
  <>
216
310
  <div class="sticky top-0 z-40">
217
- {chrome.announcement && <AnnouncementBar text={chrome.announcement} variant="static" />}
311
+ {chrome.header.announcement?.messages[0] && (
312
+ <AnnouncementBar text={chrome.header.announcement.messages[0]} variant="static" />
313
+ )}
218
314
  <SiteHeader
219
315
  logo={headerLogo}
220
- wordmark={tenant.displayName}
221
- nav={chrome.nav}
316
+ wordmark={headerWordmark}
317
+ brandHref={headerBrandHref}
318
+ nav={chrome.header.nav}
222
319
  variant={headerDark ? "dark" : "light"}
223
- announcement={chrome.announcement}
320
+ layout={resolvedHeaderVariant}
321
+ announcement={chrome.header.announcement?.messages[0]}
224
322
  searchAction={features.productSearch ? "/search" : undefined}
225
323
  savedItemsHref={features.savedItems ? "/saved" : undefined}
226
- headerCtas={chrome.headerCtas}
324
+ headerCtas={chrome.header.ctas}
227
325
  authLink={authLink}
228
326
  sticky={false}
229
327
  />
@@ -244,8 +342,9 @@ const footerWidgets = await renderWidgetSlot(
244
342
  <SiteFooter
245
343
  brand={{
246
344
  wordmark: tenant.displayName,
247
- contact: chrome.footer.tagline ? [chrome.footer.tagline] : undefined,
345
+ contact: chrome.footer.brand?.text ? [chrome.footer.brand.text] : undefined,
248
346
  }}
347
+ variant={resolvedFooterVariant}
249
348
  columns={chrome.footer.columns}
250
349
  newsletter={features.newsletter ? { title: "Newsletter", placeholder: "you@example.com" } : undefined}
251
350
  social={chrome.footer.social}
@@ -255,7 +354,7 @@ const footerWidgets = await renderWidgetSlot(
255
354
  </>
256
355
  )}
257
356
 
258
- {/* Private-app widgets (D5, global.footer) — sandboxed per install; one
357
+ {/* Private-app widgets (global.footer) — sandboxed per install; one
259
358
  resolution above covers both footer paths. */}
260
359
  {footerWidgets.map((w) => (
261
360
  <div class="container-prose py-6">
@@ -357,6 +456,8 @@ const footerWidgets = await renderWidgetSlot(
357
456
  function fix(a) {
358
457
  var href = a.getAttribute("href");
359
458
  if (!href || href.charAt(0) !== "/" || href.charAt(1) === "/") return;
459
+ // Global asset namespaces are root-served under every tenant.
460
+ if (href.indexOf("/tenants/") === 0 || href.indexOf("/shared/") === 0) return;
360
461
  if (href === base || href.indexOf(base + "/") === 0) return;
361
462
  a.setAttribute("href", base + href);
362
463
  }
@@ -381,7 +482,7 @@ const footerWidgets = await renderWidgetSlot(
381
482
  </script>
382
483
  )}
383
484
 
384
- {/* Dev-loop theme hot-patch (D1): tenant-hot-reload.mjs broadcasts a
485
+ {/* Dev-loop theme hot-patch: tenant-hot-reload.mjs broadcasts a
385
486
  `tot:theme-vars` HMR custom event on every theme.json save instead of
386
487
  forcing a full reload; swap the CSS var block in place so edits repaint
387
488
  instantly with no lost scroll position. import.meta.hot is undefined in
@@ -397,7 +498,7 @@ const footerWidgets = await renderWidgetSlot(
397
498
  </script>
398
499
  )}
399
500
 
400
- {/* Private-app widget postMessage bridge (D5) — mounted unconditionally
501
+ {/* Private-app widget postMessage bridge — mounted unconditionally
401
502
  (cheap no-op with zero tracked iframes); scans for
402
503
  `[data-tot-widget-origin]` frames itself, so it doesn't need to know
403
504
  which slots rendered above. */}
@@ -431,14 +532,38 @@ const footerWidgets = await renderWidgetSlot(
431
532
  }
432
533
  </script>
433
534
 
434
- {/* Field Web Vitals (F14 RUM) — measures LCP/INP/CLS for real visitors and
535
+ {/* Field Web Vitals (RUM) — measures LCP/INP/CLS for real visitors and
435
536
  beacons per-route samples to the RUM sink. ~1KB, no framework; no-ops
436
537
  where PerformanceObserver is absent. This is the per-route-template
437
- signal F16's `core-web-vitals` monitor consumes (the CF Web Analytics
538
+ signal the `core-web-vitals` monitor consumes (the CF Web Analytics
438
539
  beacon above only gives aggregate field CWV). */}
439
540
  <script>
440
541
  import { startWebVitals } from "@/lib/analytics/webVitals";
441
542
  startWebVitals();
442
543
  </script>
544
+
545
+ {/* Protected-surface tamper sentinel (ext-policy-09, D-R7) — a DETECTIVE
546
+ runtime control. Full-parent third-party code shares this document and its
547
+ DOM reach can't be enforceably narrowed, so this never prevents mutation: it
548
+ detects tampering of the platform-owned compliance/commerce surfaces
549
+ (policy-04 PROTECTED_SURFACES), restores safe state, evidences it, and
550
+ suspends the attributable integration. No-ops when no integration plan is
551
+ present. */}
552
+ <script>
553
+ import { bootTamperSentinel } from "@/lib/compliance/tamper-sentinel-boot";
554
+ bootTamperSentinel();
555
+ </script>
556
+
557
+ {/* document.deferred global-entry boundary (ext-kernel-04) — the last phase,
558
+ the renderer-owned mount point for the deferred integration entry. */}
559
+ <Fragment set:html={integrationPhases.bodyDeferred} />
560
+
561
+ {/* document.deferred lifecycle boot (ext-kernel-05) — initializes the plan's
562
+ integrations exactly once per document, wires the reserved completed-
563
+ navigation event, and disposes on teardown; a no-op for an empty plan. */}
564
+ <script>
565
+ import { bootIntegrationLifecycleFromDocument } from "@tot/public-runtime";
566
+ bootIntegrationLifecycleFromDocument();
567
+ </script>
443
568
  </body>
444
569
  </html>
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Operational-activity ALERT conditions (card D7) — the detection layer on top of
3
- * the D5 query surface.
2
+ * Operational-activity ALERT conditions — the detection layer on top of
3
+ * the activity query surface.
4
4
  *
5
- * D1–D5 make operational activity queryable; D7 turns four specific, high-value
6
- * conditions in that stream into ALERTS. It deliberately follows the SAME shape as
7
- * this repo's only other alert contract, F14's `analytics/rumAlert.ts`: a set of
5
+ * The query surface makes operational activity queryable; this turns four specific,
6
+ * high-value conditions in that stream into ALERTS. It follows the same shape as
7
+ * this repo's other alert contract, `analytics/rumAlert.ts`: a set of
8
8
  * PURE, transport-agnostic detectors that take a window of activity and return a
9
- * structured `ActivityAlert[]`. WHERE the alerts go (a log line, a future
9
+ * structured `ActivityAlert[]`. WHERE the alerts go (a log line, a
10
10
  * PagerDuty/Slack sink, a polled endpoint) is the sink's job — there is NO outbound
11
- * notification transport in this repo today (rumAlert is likewise detection-only),
12
- * so D7 does not invent one. It also does NOT add a cron: the checker is on-demand
11
+ * notification transport in this repo (rumAlert is likewise detection-only),
12
+ * so this layer does not invent one. It also does NOT add a cron: the checker is on-demand
13
13
  * (computed when `/admin/ops-timeline` loads, or when the read-only alerts endpoint
14
- * is polled), exactly as the card scopes it.
14
+ * is polled).
15
15
  *
16
16
  * THE FOUR CONDITIONS (and how each avoids false-positive noise):
17
17
  *
@@ -37,7 +37,7 @@
37
37
  * still legitimately in flight (received < grace ago) never fires; a terminal
38
38
  * after the received (any status) clears it.
39
39
  *
40
- * 4. deploy-skew — promotes D5's visibility-only `deriveDeploySkew()` into an
40
+ * 4. deploy-skew — promotes the visibility-only `deriveDeploySkew()` into an
41
41
  * alert. `skewDetected` is true on EVERY normal rollout (two isolates coexist
42
42
  * briefly), so alerting on it directly would page on every deploy. Instead this
43
43
  * fires ONLY when the build serving this request differs from the newest
@@ -45,12 +45,10 @@
45
45
  * period (default 15m) — i.e. THIS isolate failed to roll to a deploy that
46
46
  * finished long ago (a persistent stale isolate), not a rollout in progress.
47
47
  *
48
- * DATA DEPENDENCY (honest wiring status, mirroring F16's `WiringStatus`). The
49
- * detectors read the D1 `activity_events` timeline through the D5 `queryActivity`
50
- * seam, so they alert on whatever is in that timeline. Getting SERVER-emitted
51
- * lifecycle events INTO D1 is the D1/D4 fan-out wiring (today `recordActivity()` is
52
- * logs-only; `deployVersion.ts` notes the swap to a timeline-fanning emitter is
53
- * pending) — NOT D7's job. `preview.serve` stays `analytics`-sink by design (too
48
+ * DATA DEPENDENCY. The detectors read the D1 `activity_events` timeline through the
49
+ * `queryActivity` seam, so they alert on whatever is in that timeline. Getting
50
+ * server-emitted lifecycle events into the timeline is the fan-out wiring's job, not
51
+ * this layer's. `preview.serve` stays `analytics`-sink by design (too
54
52
  * high-volume to timeline wholesale); detector 2 is source-agnostic (the rumAlert
55
53
  * precedent) and fires against an Analytics-Engine-sourced or seeded feed. See
56
54
  * `ALERT_CONDITIONS` for the per-condition source + wiring note.
@@ -63,10 +61,10 @@ import {
63
61
  } from "./query.js";
64
62
  import type { Queryable } from "../d1/catalog.js";
65
63
 
66
- /** Severity, mirroring F14's `rumAlert.AlertSeverity` / F16's `manifest.Severity`. */
64
+ /** Severity, mirroring `rumAlert.AlertSeverity` / `manifest.Severity`. */
67
65
  export type AlertSeverity = "page" | "critical" | "warning" | "info";
68
66
 
69
- /** The four D7 conditions. Stable ids — a downstream sink/dashboard keys on them. */
67
+ /** The four conditions. Stable ids — a downstream sink/dashboard keys on them. */
70
68
  export type ActivityAlertType =
71
69
  | "silent-terminal"
72
70
  | "recurring-preview-miss"
@@ -276,7 +274,7 @@ export interface DeploySkewAlertOptions {
276
274
  }
277
275
 
278
276
  /**
279
- * Promote D5's visibility-only `DeploySkewSummary` into an alert, WITHOUT firing on
277
+ * Promote the visibility-only `DeploySkewSummary` into an alert, WITHOUT firing on
280
278
  * every normal rollout. Fires only when the build serving this request differs from
281
279
  * the newest observed build AND that newest build has been latest for longer than
282
280
  * `graceMs` — i.e. THIS isolate never rolled to a deploy that finished long ago.
@@ -0,0 +1,87 @@
1
+ import type { D1Like } from "@/lib/d1/catalog";
2
+
3
+ const DIAGNOSTIC_STUCK_AFTER_MS = 5 * 60 * 1000;
4
+
5
+ /**
6
+ * Atomically reserve one tenant/change across authorization, Gitea, and finalization.
7
+ * `expires_at` is an operator-visible stuck-work deadline, never an automatic takeover:
8
+ * a live but slow submit cannot lose ownership underneath its forge mutation.
9
+ */
10
+ export async function acquireCandidateSubmitLease(
11
+ db: D1Like,
12
+ tenantId: string,
13
+ changeId: string,
14
+ authorizationId: string,
15
+ now = new Date(),
16
+ ): Promise<boolean> {
17
+ const acquiredAt = now.toISOString();
18
+ const expiresAt = new Date(now.getTime() + DIAGNOSTIC_STUCK_AFTER_MS).toISOString();
19
+ const row = await db.prepare(`
20
+ INSERT INTO candidate_submit_leases
21
+ (tenant_id, change_id, authorization_id, status, acquired_at, expires_at)
22
+ VALUES (?, ?, ?, 'active', ?, ?)
23
+ ON CONFLICT(tenant_id, change_id) DO UPDATE SET
24
+ authorization_id = excluded.authorization_id,
25
+ status = 'active',
26
+ acquired_at = excluded.acquired_at,
27
+ expires_at = excluded.expires_at
28
+ WHERE candidate_submit_leases.status <> 'active'
29
+ OR candidate_submit_leases.authorization_id = excluded.authorization_id
30
+ RETURNING authorization_id
31
+ `).bind(tenantId, changeId, authorizationId, acquiredAt, expiresAt).first() as
32
+ { authorization_id?: string } | null;
33
+ return row?.authorization_id === authorizationId;
34
+ }
35
+
36
+ export type CandidateSubmitLeaseState = "active" | "completed" | "missing";
37
+
38
+ /** Atomically prove ownership for the callback work. Active leases are never stolen. */
39
+ export async function renewCandidateSubmitLease(
40
+ db: D1Like,
41
+ tenantId: string,
42
+ changeId: string,
43
+ authorizationId: string,
44
+ now = new Date(),
45
+ ): Promise<CandidateSubmitLeaseState> {
46
+ const current = await db.prepare(`
47
+ SELECT authorization_id, status FROM candidate_submit_leases
48
+ WHERE tenant_id = ? AND change_id = ?
49
+ `).bind(tenantId, changeId).first() as
50
+ { authorization_id?: string; status?: string } | null;
51
+ if (current?.authorization_id !== authorizationId) return "missing";
52
+ if (current.status === "completed") return "completed";
53
+ const expiresAt = new Date(now.getTime() + DIAGNOSTIC_STUCK_AFTER_MS).toISOString();
54
+ const row = await db.prepare(`
55
+ UPDATE candidate_submit_leases SET expires_at = ?
56
+ WHERE tenant_id = ? AND change_id = ? AND authorization_id = ? AND status = 'active'
57
+ RETURNING authorization_id
58
+ `).bind(expiresAt, tenantId, changeId, authorizationId).first() as
59
+ { authorization_id?: string } | null;
60
+ return row?.authorization_id === authorizationId ? "active" : "missing";
61
+ }
62
+
63
+ /** Mark the attempt immutable/idempotent while allowing a later submit to acquire. */
64
+ export async function completeCandidateSubmitLease(
65
+ db: D1Like,
66
+ tenantId: string,
67
+ changeId: string,
68
+ authorizationId: string,
69
+ ): Promise<void> {
70
+ await db.prepare(`
71
+ UPDATE candidate_submit_leases SET status = 'completed'
72
+ WHERE tenant_id = ? AND change_id = ? AND authorization_id = ? AND status = 'active'
73
+ `).bind(tenantId, changeId, authorizationId).run();
74
+ }
75
+
76
+ /** Release only this attempt; it can never unlock a newer submit. */
77
+ export async function releaseCandidateSubmitLease(
78
+ db: D1Like,
79
+ tenantId: string,
80
+ changeId: string,
81
+ authorizationId: string,
82
+ ): Promise<void> {
83
+ await db.prepare(`
84
+ DELETE FROM candidate_submit_leases
85
+ WHERE tenant_id = ? AND change_id = ? AND authorization_id = ? AND status = 'active'
86
+ `).bind(tenantId, changeId, authorizationId).run();
87
+ }
@@ -2,25 +2,36 @@
2
2
  * Owner-facing "who did this" attribution for a candidate change or aggregate —
3
3
  * DELIBERATELY separate from the anonymized activity spine. `ActivityActor.id`
4
4
  * is a salted, non-reversible hash by design (see `recordActivity.ts`'s header:
5
- * a raw PII id in the core zone would defeat the D0 contract), so it can never
5
+ * a raw PII id in the core zone would defeat the redaction contract), so it can never
6
6
  * answer "who submitted/accepted/rejected/shipped this" for an owner reviewing
7
7
  * `/admin#publish`. This persists the real email (never joined into the general
8
8
  * `queryActivity()` surface, never cross-tenant) alongside the existing hashed
9
9
  * event, so the two concerns stay separate: analytics stays anonymized, and
10
10
  * this stays a narrow, owner-only-readable accountability record.
11
11
  *
12
- * `sub` (the tot20-issued, stable user id) would be a better key than email —
13
- * email is a mutable account attribute, not the identity itself — but `sub`
14
- * is dropped before `SessionRecord` today (see `auth/session.ts`). Threading
15
- * it through is a real follow-up, not done here; email is what every call site
16
- * already has.
12
+ * MCP-submitted candidates carry the tot20-issued stable subject plus a verified
13
+ * email/display label. Storefront-local UI actions still use their authenticated
14
+ * session email until the same subject is threaded through `SessionRecord`.
17
15
  */
18
16
  import { displayNameFromEmail } from "./recordActivity";
19
17
 
20
- export type ChangeActorAction = "submit" | "reject" | "accept" | "ship" | "build";
18
+ export type ChangeActorAction = "submit" | "reject" | "resolve" | "accept" | "ship" | "build";
19
+
20
+ export interface ChangeActorIdentity {
21
+ kind: "human" | "service";
22
+ /** Immutable identity-provider subject for a person, or app subject for a service. */
23
+ subject: string;
24
+ /** Present only for a verified human identity. */
25
+ email: string | null;
26
+ /** Owner-facing label. */
27
+ name: string;
28
+ }
21
29
 
22
30
  export interface ChangeActorRecord {
23
- email: string;
31
+ /** Older records may predate structured identity and omit these two fields. */
32
+ kind?: ChangeActorIdentity["kind"];
33
+ subject?: string;
34
+ email: string | null;
24
35
  /** Best-effort display name — no real name is captured anywhere today, so
25
36
  * this is always {@link displayNameFromEmail}'s local-part guess. Swap the
26
37
  * source here (not the callers) if a real name ever becomes available. */
@@ -37,6 +48,14 @@ function attributionKey(tenantId: string, subjectId: string, action: ChangeActor
37
48
  return `cust:change-actor:${tenantId}:${subjectId}:${action}`;
38
49
  }
39
50
 
51
+ function submitAuthorizationKey(
52
+ tenantId: string,
53
+ changeId: string,
54
+ authorizationId: string,
55
+ ): string {
56
+ return `cust:change-actor-authorization:${tenantId}:${changeId}:${authorizationId}`;
57
+ }
58
+
40
59
  /**
41
60
  * Persist who took `action` on `subjectId` (a `changeId` for reject/accept/build,
42
61
  * or the aggregate id for ship). Best-effort — never blocks or fails the
@@ -51,6 +70,8 @@ export async function recordChangeActor(
51
70
  ): Promise<void> {
52
71
  try {
53
72
  const record: ChangeActorRecord = {
73
+ kind: "human",
74
+ subject: `email:${email.trim().toLowerCase()}`,
54
75
  email,
55
76
  name: displayNameFromEmail(email),
56
77
  at: new Date().toISOString(),
@@ -61,6 +82,65 @@ export async function recordChangeActor(
61
82
  }
62
83
  }
63
84
 
85
+ /**
86
+ * Persist an actor as a required part of a privileged mutation's authorization
87
+ * boundary. Unlike {@link recordChangeActor}, storage failures propagate so the
88
+ * caller can fail closed before performing the mutation.
89
+ */
90
+ export async function recordRequiredChangeActor(
91
+ kv: ChangeActorKv,
92
+ tenantId: string,
93
+ subjectId: string,
94
+ action: ChangeActorAction,
95
+ actor: ChangeActorIdentity,
96
+ at: string,
97
+ ): Promise<void> {
98
+ const record: ChangeActorRecord = { ...actor, at };
99
+ await kv.put(attributionKey(tenantId, subjectId, action), JSON.stringify(record));
100
+ }
101
+
102
+ /**
103
+ * Stage an immutable, per-attempt submit authorization before the forge write.
104
+ * This deliberately does not replace the canonical `submit` actor: a failed
105
+ * Gitea update must never make an older PR look like it was submitted by the
106
+ * actor whose later attempt failed.
107
+ */
108
+ export async function recordCandidateSubmitAuthorization(
109
+ kv: ChangeActorKv,
110
+ tenantId: string,
111
+ changeId: string,
112
+ authorizationId: string,
113
+ actor: ChangeActorIdentity,
114
+ authorizedAt: string,
115
+ ): Promise<void> {
116
+ const record: ChangeActorRecord = { ...actor, at: authorizedAt };
117
+ await kv.put(
118
+ submitAuthorizationKey(tenantId, changeId, authorizationId),
119
+ JSON.stringify(record),
120
+ );
121
+ }
122
+
123
+ /** Read one exact pre-forge authorization. Missing/corrupt data fails closed. */
124
+ export async function readCandidateSubmitAuthorization(
125
+ kv: ChangeActorKv,
126
+ tenantId: string,
127
+ changeId: string,
128
+ authorizationId: string,
129
+ ): Promise<ChangeActorRecord | null> {
130
+ try {
131
+ const raw = await kv.get(submitAuthorizationKey(tenantId, changeId, authorizationId));
132
+ return raw ? JSON.parse(raw) as ChangeActorRecord : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /** Browser responses never need the immutable identity-provider subject. */
139
+ export function ownerVisibleChangeActor(record: ChangeActorRecord): ChangeActorRecord {
140
+ const { subject: _subject, ...visible } = record;
141
+ return visible;
142
+ }
143
+
64
144
  /** Read every recorded action's attribution for one subject. Best-effort per
65
145
  * action: a missing/corrupt record is simply absent from the result, never
66
146
  * a thrown error. */
@@ -69,7 +149,7 @@ export async function readChangeActors(
69
149
  tenantId: string,
70
150
  subjectId: string,
71
151
  ): Promise<Partial<Record<ChangeActorAction, ChangeActorRecord>>> {
72
- const actions: ChangeActorAction[] = ["submit", "reject", "accept", "ship", "build"];
152
+ const actions: ChangeActorAction[] = ["submit", "reject", "resolve", "accept", "ship", "build"];
73
153
  const out: Partial<Record<ChangeActorAction, ChangeActorRecord>> = {};
74
154
  await Promise.all(
75
155
  actions.map(async (action) => {