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

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 (370) hide show
  1. package/apps/storefront/astro.config.mjs +3 -3
  2. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +8 -3
  3. package/apps/storefront/env.d.ts +54 -5
  4. package/apps/storefront/integrations/materialize-guard.mjs +12 -14
  5. package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
  6. package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
  7. package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
  8. package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
  9. package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
  10. package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
  11. package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
  12. package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
  13. package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
  14. package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
  15. package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
  16. package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
  17. package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
  18. package/apps/storefront/migrations-apps/meta/_journal.json +42 -0
  19. package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
  20. package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
  21. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
  22. package/apps/storefront/public/shared/commerce-chrome.css +20 -2
  23. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
  24. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
  25. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
  26. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
  27. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
  28. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
  29. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
  30. package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
  31. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
  32. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
  33. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
  34. package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
  35. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
  36. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
  37. package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
  38. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
  39. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
  40. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
  41. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
  42. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
  43. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
  44. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
  45. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
  46. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
  47. package/apps/storefront/src/components/Img.astro +1 -1
  48. package/apps/storefront/src/components/admin/AdminBlogTab.astro +19 -16
  49. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
  50. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1057 -412
  51. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  52. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
  53. package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
  54. package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
  55. package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
  59. package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
  60. package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
  61. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
  62. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
  63. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
  64. package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
  65. package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
  66. package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
  67. package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
  68. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
  69. package/apps/storefront/src/components/membership/TierCards.astro +2 -2
  70. package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
  71. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
  72. package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
  73. package/apps/storefront/src/config/devTenantSeed.ts +9 -9
  74. package/apps/storefront/src/config/resolver.ts +18 -10
  75. package/apps/storefront/src/layouts/Layout.astro +100 -20
  76. package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
  77. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +86 -6
  78. package/apps/storefront/src/lib/activity/deployVersion.ts +1 -1
  79. package/apps/storefront/src/lib/admin/adminShell.ts +2 -0
  80. package/apps/storefront/src/lib/admin/envelope.ts +1 -2
  81. package/apps/storefront/src/lib/apps/adminService.ts +10 -0
  82. package/apps/storefront/src/lib/apps/adminSession.ts +1 -1
  83. package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
  84. package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
  85. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
  86. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
  87. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
  88. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
  89. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
  90. package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
  91. package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
  92. package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
  93. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
  94. package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
  95. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +8 -0
  96. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +1 -1
  97. package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
  98. package/apps/storefront/src/lib/apps/registryService.ts +264 -10
  99. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
  100. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
  101. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
  102. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
  103. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
  104. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
  105. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
  106. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
  107. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
  108. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
  109. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
  110. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
  111. package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
  112. package/apps/storefront/src/lib/auth/adminEntry.ts +2 -2
  113. package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
  114. package/apps/storefront/src/lib/blog/collection.ts +114 -17
  115. package/apps/storefront/src/lib/blog/markdown.ts +1 -1
  116. package/apps/storefront/src/lib/blog/provider.ts +76 -9
  117. package/apps/storefront/src/lib/blog/types.ts +9 -9
  118. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
  119. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
  120. package/apps/storefront/src/lib/chrome/model.ts +48 -253
  121. package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
  122. package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
  123. package/apps/storefront/src/lib/chrome/variant.ts +39 -0
  124. package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
  125. package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
  126. package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
  127. package/apps/storefront/src/lib/content/index.ts +5 -5
  128. package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
  129. package/apps/storefront/src/lib/content-edit/route.ts +1 -1
  130. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  131. package/apps/storefront/src/lib/d1/schema-apps.ts +276 -0
  132. package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
  133. package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
  134. package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
  135. package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
  136. package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
  137. package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
  138. package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
  139. package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
  140. package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
  141. package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
  142. package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
  143. package/apps/storefront/src/lib/edgeCache.ts +18 -6
  144. package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
  145. package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
  146. package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
  147. package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
  148. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
  149. package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
  150. package/apps/storefront/src/lib/membership/model.ts +1 -2
  151. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +2 -2
  152. package/apps/storefront/src/lib/newsletter/consent.ts +2 -2
  153. package/apps/storefront/src/lib/newsletter/send.ts +3 -3
  154. package/apps/storefront/src/lib/previewReload.ts +57 -1
  155. package/apps/storefront/src/lib/publish/adminPublishTab.ts +1775 -669
  156. package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
  157. package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
  158. package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
  159. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
  160. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
  161. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
  162. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
  163. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
  164. package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
  165. package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
  166. package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
  167. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
  168. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
  169. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
  170. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
  171. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
  172. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
  173. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
  174. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
  175. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
  176. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
  177. package/apps/storefront/src/lib/publish/shipWorkspace.ts +22 -3
  178. package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
  179. package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
  180. package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
  181. package/apps/storefront/src/lib/rawChrome.ts +125 -47
  182. package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
  183. package/apps/storefront/src/lib/seo/alternates.ts +1 -1
  184. package/apps/storefront/src/lib/social/notify.ts +4 -4
  185. package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
  186. package/apps/storefront/src/lib/storyblok/provider.ts +203 -69
  187. package/apps/storefront/src/lib/subscription/model.ts +4 -4
  188. package/apps/storefront/src/lib/the-build/provider.ts +9 -4
  189. package/apps/storefront/src/middleware/index.ts +273 -51
  190. package/apps/storefront/src/pages/[...slug].astro +9 -4
  191. package/apps/storefront/src/pages/admin/review-release.astro +265 -0
  192. package/apps/storefront/src/pages/admin/select.astro +96 -0
  193. package/apps/storefront/src/pages/admin.astro +16 -2
  194. package/apps/storefront/src/pages/api/activity.ts +8 -8
  195. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
  196. package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
  197. package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
  198. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
  199. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
  200. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
  201. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
  202. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
  203. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
  204. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
  205. package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
  206. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
  207. package/apps/storefront/src/pages/api/rum/vitals.ts +1 -2
  208. package/apps/storefront/src/pages/blog/[slug].astro +5 -5
  209. package/apps/storefront/src/pages/blog/author/[author].astro +3 -3
  210. package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
  211. package/apps/storefront/src/pages/blog/index.astro +2 -2
  212. package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
  213. package/apps/storefront/src/pages/cockpit.astro +61 -1362
  214. package/apps/storefront/src/pages/index.astro +10 -5
  215. package/apps/storefront/src/pages/llms.txt.ts +1 -1
  216. package/apps/storefront/src/pages/products/[handle].astro +7 -6
  217. package/apps/storefront/src/pages/sitemap.xml.ts +2 -2
  218. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +42 -31
  219. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +8 -3
  220. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +6 -2
  221. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +6 -2
  222. package/apps/storefront/src/pages/style-guide/index.astro +19 -5
  223. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
  224. package/apps/storefront/src/pages/the-build/[slug].astro +4 -4
  225. package/apps/storefront/src/pages/the-build/index.astro +2 -2
  226. package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
  227. package/apps/storefront/src/styles/admin.css +27 -1
  228. package/apps/storefront/src/themes/tenants/index.ts +19 -7
  229. package/apps/storefront/tsconfig.json +1 -1
  230. package/package.json +1 -1
  231. package/packages/public-runtime/package.json +2 -1
  232. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
  233. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
  234. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
  235. package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
  236. package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
  237. package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
  238. package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
  239. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
  240. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
  241. package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
  242. package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
  243. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
  244. package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
  245. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
  246. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
  247. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
  248. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
  249. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
  250. package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
  251. package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
  252. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
  253. package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
  254. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
  255. package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
  256. package/packages/public-runtime/src/activity/catalog.ts +41 -37
  257. package/packages/public-runtime/src/activity/event.ts +6 -6
  258. package/packages/public-runtime/src/activity/index.ts +2 -2
  259. package/packages/public-runtime/src/activity/redaction.ts +4 -4
  260. package/packages/public-runtime/src/candidate-index.ts +42 -8
  261. package/packages/public-runtime/src/checkout-orders.ts +2 -2
  262. package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
  263. package/packages/public-runtime/src/checkout-style.ts +7 -7
  264. package/packages/public-runtime/src/checkout.ts +3 -3
  265. package/packages/public-runtime/src/chrome.ts +777 -0
  266. package/packages/public-runtime/src/compliance-obligations.ts +310 -0
  267. package/packages/public-runtime/src/csp.ts +99 -0
  268. package/packages/public-runtime/src/customization-runtime.ts +32 -1
  269. package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
  270. package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
  271. package/packages/public-runtime/src/embed-catalog.mjs +40 -0
  272. package/packages/public-runtime/src/evidence.ts +288 -0
  273. package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
  274. package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
  275. package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
  276. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
  277. package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
  278. package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
  279. package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
  280. package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
  281. package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
  282. package/packages/public-runtime/src/index.ts +16 -1
  283. package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
  284. package/packages/public-runtime/src/integration/assurance.ts +228 -0
  285. package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
  286. package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
  287. package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
  288. package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
  289. package/packages/public-runtime/src/integration/consent-config.ts +260 -0
  290. package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
  291. package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
  292. package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
  293. package/packages/public-runtime/src/integration/csp.ts +105 -0
  294. package/packages/public-runtime/src/integration/digest.ts +43 -0
  295. package/packages/public-runtime/src/integration/environment.ts +228 -0
  296. package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
  297. package/packages/public-runtime/src/integration/events.ts +701 -0
  298. package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
  299. package/packages/public-runtime/src/integration/index.ts +90 -0
  300. package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
  301. package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
  302. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
  303. package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
  304. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
  305. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
  306. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
  307. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
  308. package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
  309. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
  310. package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
  311. package/packages/public-runtime/src/integration/manifest.ts +470 -0
  312. package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
  313. package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
  314. package/packages/public-runtime/src/integration/observability.ts +451 -0
  315. package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
  316. package/packages/public-runtime/src/integration/phases.ts +182 -0
  317. package/packages/public-runtime/src/integration/plan.ts +112 -0
  318. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
  319. package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
  320. package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
  321. package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
  322. package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
  323. package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
  324. package/packages/public-runtime/src/integration/render-phases.ts +267 -0
  325. package/packages/public-runtime/src/integration/resolve.ts +465 -0
  326. package/packages/public-runtime/src/integration/result.ts +239 -0
  327. package/packages/public-runtime/src/integration/secrets.ts +92 -0
  328. package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
  329. package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
  330. package/packages/public-runtime/src/integration/version.ts +92 -0
  331. package/packages/public-runtime/src/integration-capabilities.ts +520 -0
  332. package/packages/public-runtime/src/membership.ts +2 -2
  333. package/packages/public-runtime/src/raw-html-policy.ts +2 -4
  334. package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
  335. package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
  336. package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
  337. package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
  338. package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
  339. package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
  340. package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
  341. package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
  342. package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
  343. package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
  344. package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
  345. package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
  346. package/packages/public-runtime/src/static-bundle.ts +5 -5
  347. package/packages/public-runtime/src/tenant-assets.ts +143 -4
  348. package/packages/public-runtime/src/tenant.ts +136 -9
  349. package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
  350. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
  351. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
  352. package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
  353. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
  354. package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
  355. package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
  356. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
  357. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
  358. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
  359. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
  360. package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
  361. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
  362. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
  363. package/scripts/build/copy-tenant-assets.mjs +12 -5
  364. package/scripts/dev/ai-edit.mjs +2 -1
  365. package/scripts/dev/file-browser.mjs +2 -2
  366. package/scripts/dev/file-writer.mjs +2 -2
  367. package/scripts/dev/integration-guardrails.mjs +498 -0
  368. package/scripts/dev/safe-path.mjs +2 -2
  369. package/scripts/dev/tenant-source-root.mjs +3 -2
  370. package/scripts/tot-dev.mjs +5 -4
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Cross-domain consent propagation for hosted-receipt measurement (ADR 0022
3
+ * `receipt-consent-propagation`, ADR 0023 Q3 `analytics-consent-required`;
4
+ * ext-commerce-06). The "receipt" is the post-purchase page the PROVIDER (Foxy)
5
+ * renders on ITS OWN origin — a DIFFERENT domain from the storefront — so any
6
+ * browser measurement there is cross-domain.
7
+ *
8
+ * This is the decision seam that says whether that cross-domain browser measurement
9
+ * may run, and if so, exactly what may cross the domain boundary with it. It is
10
+ * DEFAULT-SAFE to server-side-only conversion: the storefront's own verified-order
11
+ * → server conversion join ({@link ./order-conversion.ts}) is the authoritative,
12
+ * always-on path; receipt-page BROWSER measurement is an additive best-effort layer
13
+ * that is enabled ONLY when every gate below holds, and is silently skipped (server
14
+ * conversion still fires) otherwise.
15
+ *
16
+ * Two BINDING board decisions gate it, both fail-closed:
17
+ *
18
+ * - `integration-receipt-consent`: non-essential receipt-page browser measurement
19
+ * requires VERIFIED provider-supported consent propagation. Until provider
20
+ * discovery + counsel verify that the provider honours and propagates the
21
+ * shopper's analytics-consent decision on the receipt origin, conversion stays
22
+ * SERVER-SIDE ONLY. {@link ProviderReceiptSupport.consentPropagationVerified}
23
+ * defaults CLOSED (unverified), so with no verified provider integration this
24
+ * seam always returns `server-only`.
25
+ *
26
+ * - `gate-legal-q3` (analytics-consent-required): the shopper's analytics-consent
27
+ * decision (Q3) must TRAVEL WITH, and GATE, any browser measurement. When the
28
+ * shopper is not analytics-eligible, no browser measurement runs — attribution
29
+ * is dropped and conversion is server-side only.
30
+ *
31
+ * What crosses the boundary is a {@link ReceiptMeasurementGrant}: a bounded, opaque,
32
+ * CLOSED descriptor carrying ONLY public execution-context facts (tenant, stage,
33
+ * provider), the OPAQUE bounded correlation THREAD id (never the signed correlation
34
+ * TOKEN, never the signing secret, never a re-usable capability), and the
35
+ * analytics-consent snapshot that travels with the measurement. It carries NO PII
36
+ * and NO secret by construction — the shape has no such field and
37
+ * {@link findForbiddenFields} is asserted over every serialized grant.
38
+ *
39
+ * Pure and dependency-injected: the provider-support fact and the correlation thread
40
+ * are inputs from trusted server context, so the whole decision is exercised in node
41
+ * with no edge runtime.
42
+ */
43
+ import type { IntegrationEnvironment } from "./environment.js";
44
+ import { INTEGRATION_ENVIRONMENTS } from "./environment.js";
45
+ import type { ConsentSnapshot } from "./consent-broker.js";
46
+ import { isPurposeEligible, toEventConsentSnapshot } from "./consent-broker.js";
47
+ import type { EventConsentSnapshot, EventCorrelation } from "./events.js";
48
+ import type { ConsentPurpose } from "../integration-capabilities.js";
49
+ import { CONSENT_PURPOSES } from "../integration-capabilities.js";
50
+ import { RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION, CONSENT_SNAPSHOT_SCHEMA_VERSION } from "./version.js";
51
+ import { findForbiddenFields } from "./secrets.js";
52
+
53
+ /** The consent purpose receipt-page browser measurement requires (ADR 0023 Q3). */
54
+ export const RECEIPT_MEASUREMENT_CONSENT_PURPOSE: ConsentPurpose = "analytics";
55
+
56
+ /**
57
+ * The server-verified fact of whether a provider's hosted receipt supports propagating
58
+ * the shopper's analytics-consent decision to browser measurement on the receipt origin.
59
+ * This is NOT a caller assertion the browser can set — it is resolved from trusted
60
+ * server context (a provider-integration registry gated by counsel + discovery).
61
+ */
62
+ export interface ProviderReceiptSupport {
63
+ /** The provider whose hosted receipt this is (e.g. `"foxy"`) — a public fact. */
64
+ provider: string;
65
+ /**
66
+ * Whether this provider's receipt has VERIFIED consent-propagation support. DEFAULT
67
+ * CLOSED: absent/unknown ⇒ `false` ⇒ server-side conversion only. Only a
68
+ * counsel-and-discovery-verified provider integration flips this `true`.
69
+ */
70
+ consentPropagationVerified: boolean;
71
+ }
72
+
73
+ /**
74
+ * The bounded, opaque descriptor that crosses storefront → provider → app domains to
75
+ * enable receipt-page browser measurement. CLOSED and browser-safe: it carries ONLY
76
+ * public execution-context facts, the opaque correlation THREAD id, its bounded expiry,
77
+ * and the analytics-consent snapshot that travels with the measurement. It carries no
78
+ * PII, no signing secret, and no re-usable token — the correlation thread id verifies
79
+ * nothing on its own.
80
+ */
81
+ export interface ReceiptMeasurementGrant {
82
+ schemaVersion: typeof RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION;
83
+ /** The provider whose receipt origin the measurement runs on — a public fact. */
84
+ provider: string;
85
+ /** The tenant the handoff ran in — a public execution-context fact. */
86
+ tenant: string;
87
+ /** The stage the handoff ran in; a staged grant can never enable a live receipt read. */
88
+ environment: IntegrationEnvironment;
89
+ /**
90
+ * The OPAQUE bounded correlation thread id (the handoff `eventId`) — the ONE
91
+ * cross-site attribution handle. NOT the signed correlation token, NOT a capability:
92
+ * it verifies nothing and authorizes nothing; it only threads this measurement to its
93
+ * authoritative server conversion.
94
+ */
95
+ correlationId: string;
96
+ /** The correlation's bounded expiry (ISO 8601). Measurement past this is dropped. */
97
+ correlationExpiresAt: string;
98
+ /**
99
+ * The analytics-consent snapshot that TRAVELS WITH the measurement (Q3): eligible
100
+ * purpose names + snapshot id, never raw consent state and never PII.
101
+ */
102
+ consent: EventConsentSnapshot;
103
+ }
104
+
105
+ /** Why receipt-page browser measurement was not enabled (server-side conversion only). */
106
+ export type ReceiptMeasurementDenyReason =
107
+ /** The shopper is not analytics-eligible (Q3 gate) — attribution dropped. */
108
+ | "analytics-consent-required"
109
+ /** The provider has no VERIFIED consent-propagation support (board decision). */
110
+ | "provider-unverified"
111
+ /** No opaque correlation thread to carry (the handoff was unattributed). */
112
+ | "correlation-missing"
113
+ /** The correlation thread is past its bounded lifetime. */
114
+ | "correlation-expired"
115
+ /** The grant's tenant/stage does not match the receipt-read's trusted context. */
116
+ | "context-mismatch";
117
+
118
+ /**
119
+ * The receipt-measurement decision. `browser` carries the bounded {@link
120
+ * ReceiptMeasurementGrant} to propagate across the domain boundary; `server-only` is
121
+ * the default-safe outcome — no browser measurement, the server conversion join alone.
122
+ */
123
+ export type ReceiptMeasurementDecision =
124
+ | { mode: "browser"; grant: ReceiptMeasurementGrant }
125
+ | { mode: "server-only"; reason: ReceiptMeasurementDenyReason };
126
+
127
+ export interface ResolveReceiptMeasurementInput {
128
+ /** The tenant, from trusted server execution context — never a client value. */
129
+ tenant: string;
130
+ /** The stage, from the renderer-owned `BrowserEnvironment.environment` (server-supplied). */
131
+ environment: IntegrationEnvironment;
132
+ /** The shopper's LIVE consent snapshot (parsed fail-closed by the caller). */
133
+ consent: ConsentSnapshot;
134
+ /**
135
+ * The provider's server-verified receipt-consent-propagation support. `null`/`undefined`
136
+ * ⇒ unverified ⇒ server-side only (the expected state until Foxy discovery + counsel).
137
+ */
138
+ providerSupport: ProviderReceiptSupport | null | undefined;
139
+ /**
140
+ * The opaque bounded correlation thread from the attributed checkout handoff. `null`/
141
+ * `undefined` ⇒ the handoff was unattributed ⇒ nothing to carry ⇒ server-side only.
142
+ */
143
+ correlation: EventCorrelation | null | undefined;
144
+ /** Injectable wall clock in ms (defaults to `Date.now()`) for deterministic tests. */
145
+ now?: number;
146
+ }
147
+
148
+ /** Parse an ISO timestamp to epoch ms, or `null` if it is not a valid date. */
149
+ function parseIsoMs(iso: string): number | null {
150
+ const ms = Date.parse(iso);
151
+ return Number.isFinite(ms) ? ms : null;
152
+ }
153
+
154
+ /**
155
+ * Decide whether cross-domain browser measurement may run on the hosted receipt, and
156
+ * build the bounded grant that crosses the boundary if so. Fail-closed and
157
+ * default-safe: the gates run consent FIRST (the provider-support fact and the
158
+ * correlation are never even consulted for a shopper who withheld analytics consent),
159
+ * then verified provider support, then a present + unexpired correlation thread. Any
160
+ * gate that does not hold yields `server-only` — the authoritative server conversion
161
+ * still fires; only the additive browser layer is withheld. Never throws.
162
+ */
163
+ export function resolveReceiptMeasurement(
164
+ input: ResolveReceiptMeasurementInput,
165
+ ): ReceiptMeasurementDecision {
166
+ // Q3 consent gate FIRST: a shopper who is not analytics-eligible gets NO browser
167
+ // measurement; the provider fact and correlation are never consulted for them.
168
+ if (!isPurposeEligible(input.consent, RECEIPT_MEASUREMENT_CONSENT_PURPOSE)) {
169
+ return { mode: "server-only", reason: "analytics-consent-required" };
170
+ }
171
+
172
+ // Board decision `integration-receipt-consent`: no browser measurement without
173
+ // VERIFIED provider-supported consent propagation. Default-closed.
174
+ if (!input.providerSupport?.consentPropagationVerified) {
175
+ return { mode: "server-only", reason: "provider-unverified" };
176
+ }
177
+
178
+ // Nothing to correlate: an unattributed handoff carries no opaque thread.
179
+ const correlation = input.correlation;
180
+ if (!correlation || typeof correlation.id !== "string" || correlation.id.length === 0) {
181
+ return { mode: "server-only", reason: "correlation-missing" };
182
+ }
183
+
184
+ // Bounded lifetime: never mint a grant whose correlation thread is already dead.
185
+ const expMs = parseIsoMs(correlation.expiresAt);
186
+ const nowMs = input.now ?? Date.now();
187
+ if (expMs === null || nowMs >= expMs) {
188
+ return { mode: "server-only", reason: "correlation-expired" };
189
+ }
190
+
191
+ const grant: ReceiptMeasurementGrant = {
192
+ schemaVersion: RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION,
193
+ provider: input.providerSupport.provider,
194
+ tenant: input.tenant,
195
+ environment: input.environment,
196
+ correlationId: correlation.id,
197
+ correlationExpiresAt: correlation.expiresAt,
198
+ // The analytics-consent decision travels WITH the measurement.
199
+ consent: toEventConsentSnapshot(input.consent),
200
+ };
201
+ return { mode: "browser", grant };
202
+ }
203
+
204
+ // --- cross-domain carry -----------------------------------------------------------
205
+
206
+ /**
207
+ * Serialize a grant for propagation across the domain boundary (a redirect param or a
208
+ * `postMessage` payload). Asserts, defense-in-depth, that no field name denotes secret
209
+ * material before it can cross — so a future field addition that names a secret is
210
+ * caught here, not in production. The shape is already CLOSED and PII-free by design.
211
+ */
212
+ export function serializeReceiptMeasurementGrant(grant: ReceiptMeasurementGrant): string {
213
+ const forbidden = findForbiddenFields(grant);
214
+ if (forbidden.length > 0) {
215
+ throw new Error(
216
+ `receipt measurement grant carries forbidden field(s): ${forbidden.map((f) => f.path).join(", ")}`,
217
+ );
218
+ }
219
+ // Fixed key order so an equal grant always serializes to byte-identical output.
220
+ return JSON.stringify({
221
+ schemaVersion: grant.schemaVersion,
222
+ provider: grant.provider,
223
+ tenant: grant.tenant,
224
+ environment: grant.environment,
225
+ correlationId: grant.correlationId,
226
+ correlationExpiresAt: grant.correlationExpiresAt,
227
+ consent: {
228
+ schemaVersion: grant.consent.schemaVersion,
229
+ snapshotId: grant.consent.snapshotId,
230
+ eligiblePurposes: [...grant.consent.eligiblePurposes],
231
+ },
232
+ });
233
+ }
234
+
235
+ const GRANT_KEYS: readonly (keyof ReceiptMeasurementGrant)[] = [
236
+ "schemaVersion",
237
+ "provider",
238
+ "tenant",
239
+ "environment",
240
+ "correlationId",
241
+ "correlationExpiresAt",
242
+ "consent",
243
+ ];
244
+
245
+ /** Validate an {@link EventConsentSnapshot} fail-closed: shape + only known purposes. */
246
+ function parseEventConsentSnapshot(raw: unknown): EventConsentSnapshot | null {
247
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
248
+ const obj = raw as Record<string, unknown>;
249
+ for (const key of Object.keys(obj)) {
250
+ if (!["schemaVersion", "snapshotId", "eligiblePurposes"].includes(key)) return null;
251
+ }
252
+ if (obj.schemaVersion !== CONSENT_SNAPSHOT_SCHEMA_VERSION) return null;
253
+ if (typeof obj.snapshotId !== "string" || obj.snapshotId.length === 0) return null;
254
+ if (!Array.isArray(obj.eligiblePurposes)) return null;
255
+ const known = new Set<string>(CONSENT_PURPOSES);
256
+ for (const p of obj.eligiblePurposes) {
257
+ if (typeof p !== "string" || !known.has(p)) return null;
258
+ }
259
+ return {
260
+ schemaVersion: CONSENT_SNAPSHOT_SCHEMA_VERSION,
261
+ snapshotId: obj.snapshotId,
262
+ eligiblePurposes: [...(obj.eligiblePurposes as string[])],
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Parse a serialized grant fail-closed. An unknown schema version, an unknown or missing
268
+ * field, an invalid stage, a malformed consent snapshot, a non-string opaque handle, or
269
+ * ANY field name that denotes secret material yields `null` — a caller on the receipt
270
+ * origin cannot smuggle a forged, tampered, or secret-bearing grant into measurement.
271
+ */
272
+ export function parseReceiptMeasurementGrant(raw: unknown): ReceiptMeasurementGrant | null {
273
+ let obj: unknown = raw;
274
+ if (typeof raw === "string") {
275
+ try {
276
+ obj = JSON.parse(raw);
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return null;
282
+ const o = obj as Record<string, unknown>;
283
+
284
+ for (const key of Object.keys(o)) {
285
+ if (!(GRANT_KEYS as readonly string[]).includes(key)) return null;
286
+ }
287
+ // Defense-in-depth: reject any grant carrying a secret-named field, at any depth.
288
+ if (findForbiddenFields(o).length > 0) return null;
289
+
290
+ if (o.schemaVersion !== RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION) return null;
291
+ if (typeof o.provider !== "string" || o.provider.length === 0) return null;
292
+ if (typeof o.tenant !== "string" || o.tenant.length === 0) return null;
293
+ if (
294
+ typeof o.environment !== "string" ||
295
+ !(INTEGRATION_ENVIRONMENTS as readonly string[]).includes(o.environment)
296
+ ) {
297
+ return null;
298
+ }
299
+ if (typeof o.correlationId !== "string" || o.correlationId.length === 0) return null;
300
+ if (typeof o.correlationExpiresAt !== "string" || parseIsoMs(o.correlationExpiresAt) === null) {
301
+ return null;
302
+ }
303
+ const consent = parseEventConsentSnapshot(o.consent);
304
+ if (!consent) return null;
305
+
306
+ return {
307
+ schemaVersion: RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION,
308
+ provider: o.provider,
309
+ tenant: o.tenant,
310
+ environment: o.environment as IntegrationEnvironment,
311
+ correlationId: o.correlationId,
312
+ correlationExpiresAt: o.correlationExpiresAt,
313
+ consent,
314
+ };
315
+ }
316
+
317
+ /** The trusted receipt-origin context a consumed grant is re-checked against. */
318
+ export interface ReceiptMeasurementContext {
319
+ /** The tenant, from the receipt read's trusted context. */
320
+ tenant: string;
321
+ /** The stage, from the receipt read's trusted context. */
322
+ environment: IntegrationEnvironment;
323
+ /** Injectable wall clock in ms (defaults to `Date.now()`). */
324
+ now?: number;
325
+ }
326
+
327
+ /**
328
+ * The consume-side gate on the receipt origin. `enabled: true` means browser
329
+ * measurement may fire with the parsed grant; `enabled: false` means it must not (the
330
+ * server conversion already covers it). `malformed` is a grant that failed the
331
+ * fail-closed parse.
332
+ */
333
+ export type ReceiptMeasurementGate =
334
+ | { enabled: true; grant: ReceiptMeasurementGrant }
335
+ | { enabled: false; reason: ReceiptMeasurementDenyReason | "malformed" };
336
+
337
+ /**
338
+ * Re-validate a propagated grant on the receipt origin before any browser measurement
339
+ * fires — defense-in-depth across the domain boundary, independent of the producer.
340
+ * Fail-closed: it re-checks that analytics consent still travels with the grant, that
341
+ * the grant is bound to THIS tenant/stage (env-consistency — a staged grant can never
342
+ * enable a live receipt read), and that the correlation thread has not expired. Any
343
+ * failure ⇒ `enabled: false` ⇒ server-side conversion only.
344
+ */
345
+ export function receiptMeasurementEnabled(
346
+ rawGrant: unknown,
347
+ ctx: ReceiptMeasurementContext,
348
+ ): ReceiptMeasurementGate {
349
+ const grant = parseReceiptMeasurementGrant(rawGrant);
350
+ if (!grant) return { enabled: false, reason: "malformed" };
351
+
352
+ // Q3 consent must STILL travel with the grant.
353
+ if (!grant.consent.eligiblePurposes.includes(RECEIPT_MEASUREMENT_CONSENT_PURPOSE)) {
354
+ return { enabled: false, reason: "analytics-consent-required" };
355
+ }
356
+
357
+ // Env-consistency: the grant is bound to the tenant/stage it was minted in; a
358
+ // cross-tenant or cross-stage grant can never enable measurement here.
359
+ if (grant.tenant !== ctx.tenant || grant.environment !== ctx.environment) {
360
+ return { enabled: false, reason: "context-mismatch" };
361
+ }
362
+
363
+ const expMs = parseIsoMs(grant.correlationExpiresAt);
364
+ const nowMs = ctx.now ?? Date.now();
365
+ if (expMs === null || nowMs >= expMs) {
366
+ return { enabled: false, reason: "correlation-expired" };
367
+ }
368
+
369
+ return { enabled: true, grant };
370
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Regulated elevated-parent CERTIFICATION enforcement (ext-policy-07; ADR 0016 G2,
3
+ * ADR 0023 Q6, decision `integration-sandbox-first-surface-matrix`).
4
+ *
5
+ * Full parent authority (`elevated-parent`) is a review-only tier — its DOM reach
6
+ * is not enforceably narrow. On a `regulated` surface it is therefore DISABLED BY
7
+ * DEFAULT and may run ONLY behind a green certification that:
8
+ *
9
+ * (a) ADVERSARIALLY proves EACH server-derived compliance obligation (ext-policy-04)
10
+ * is satisfied — fail-closed on any obligation that is unproven, absent, or
11
+ * merely asserted without a withstood adversarial challenge;
12
+ * (b) is IMMUTABLY PINNED to an exact content digest — a mutable container (no
13
+ * pinned digest, a malformed digest, or merchant-mutable parent code) is
14
+ * INELIGIBLE and rejected, never "certified" (Q6);
15
+ * (c) auto-disables on any DRIFT — the digest actually being served must equal the
16
+ * certified pinned digest, or the certification no longer holds;
17
+ * (d) never certifies mutable parent code on regulated regardless of anything else.
18
+ *
19
+ * This module does NOT re-implement the assurance or promotion layers — it PRODUCES
20
+ * the `certifiedImmutable` flag those layers already consume:
21
+ * - `IntegrationContext.certifiedImmutable` (../integration-capabilities.ts), which
22
+ * turns the `cert-required` matrix cell from `elevated-parent-uncertified` into a
23
+ * grant; and
24
+ * - `AssuranceDecisionContext.certifiedImmutable` (./assurance-decision.ts), the G2
25
+ * clause that a regulated elevated-parent record needs proof, never a mere
26
+ * acknowledgement.
27
+ * The required obligation set is SERVER-DERIVED from authoritative tenant config via
28
+ * ext-policy-04's `resolveObligationIds` / `verifyObligationCoverage` — never taken
29
+ * from the (spoofable) certificate — so nothing external can drop a duty.
30
+ */
31
+ import type { CodeForm, Surface } from "../integration-capabilities.js";
32
+ import {
33
+ isRegulated,
34
+ resolveObligationIds,
35
+ verifyObligationCoverage,
36
+ type ObligationTenant,
37
+ } from "../compliance-obligations.js";
38
+ import { digestValue } from "./digest.js";
39
+ import type { ExecutionTier } from "./phases.js";
40
+
41
+ /** Contract token — bump the minor for additive vocabulary, the major for a break (ADR 0006 semver). */
42
+ export const REGULATED_PARENT_CERT_MODEL = "regulated-parent-cert-model@1" as const;
43
+ export type RegulatedParentCertModel = typeof REGULATED_PARENT_CERT_MODEL;
44
+
45
+ /**
46
+ * The outcome of an adversarial challenge run against one obligation. Only
47
+ * `withstood` — the obligation held under an active attempt to bypass it — is
48
+ * accepted. `breached` (the attempt succeeded) and `not-run` (no adversarial test
49
+ * was performed) both fail closed: an unproven obligation is never "satisfied".
50
+ */
51
+ export const OBLIGATION_PROOF_RESULTS = ["withstood", "breached", "not-run"] as const;
52
+ export type ObligationProofResult = (typeof OBLIGATION_PROOF_RESULTS)[number];
53
+
54
+ /**
55
+ * The adversarial proof for a single compliance obligation. It is not a claim that
56
+ * the duty is met — it is evidence that an adversarial challenge tried to defeat the
57
+ * duty and could not. `evidenceDigest` pins that evidence to exact content so the
58
+ * proof cannot be swapped after review.
59
+ */
60
+ export interface ObligationProof {
61
+ /** The obligation this proves — must be a server-recognized obligation id (ext-policy-04). */
62
+ obligationId: string;
63
+ /** The adversarial challenge that was run (what attack the obligation withstood). Non-empty. */
64
+ adversarialChallenge: string;
65
+ /** The challenge outcome. Anything other than `withstood` fails closed. */
66
+ result: ObligationProofResult;
67
+ /** Content digest pinning the evidence backing this proof. Must be a well-formed pinned digest. */
68
+ evidenceDigest: string;
69
+ }
70
+
71
+ /**
72
+ * A regulated elevated-parent certification artifact. It is bound to one app+version,
73
+ * pinned to one immutable content digest, scoped to the `regulated` surface and the
74
+ * `elevated-parent` tier, and carries one adversarial proof per applicable obligation.
75
+ */
76
+ export interface RegulatedParentCertification {
77
+ model: RegulatedParentCertModel;
78
+ appId: string;
79
+ version: string;
80
+ /** Must be `regulated`; a certificate for any other surface does not enable regulated parent code. */
81
+ surface: Surface;
82
+ /** Must be `elevated-parent`; this certification exists only for full-parent authority. */
83
+ executionTier: ExecutionTier;
84
+ /** Must be `pinned-immutable`; `mutable-container` is prohibited on regulated (Q6). */
85
+ codeForm: CodeForm;
86
+ /** The exact content digest the certified artifact is immutably pinned to. */
87
+ pinnedDigest: string;
88
+ /** One adversarial proof per applicable compliance obligation (ext-policy-04). */
89
+ proofs: readonly ObligationProof[];
90
+ certifierId: string;
91
+ certifiedAt: string;
92
+ }
93
+
94
+ /** Server-resolved evaluation context for a certification decision. */
95
+ export interface RegulatedParentCertContext {
96
+ /** The tenant's authoritative config — the ONLY source of the required obligation set + regulated classification. */
97
+ tenant: ObligationTenant;
98
+ /**
99
+ * The content digest of the artifact currently resolved to serve. When present and
100
+ * different from the certified `pinnedDigest`, the served code has DRIFTED from what
101
+ * was certified and enablement auto-disables (c).
102
+ */
103
+ servedDigest?: string;
104
+ /**
105
+ * The integration being evaluated. A certificate whose app/version does not match is
106
+ * for a different artifact and does not cover this one.
107
+ */
108
+ binding?: { appId: string; version: string };
109
+ }
110
+
111
+ export type RegulatedParentCertDenialReason =
112
+ /** The tenant is not on the regulated surface — this certification gate does not apply here. */
113
+ | "not-regulated"
114
+ /** No certification supplied — regulated elevated-parent is disabled by default (a). */
115
+ | "certificate-absent"
116
+ | "unknown-model"
117
+ | "wrong-surface"
118
+ | "not-elevated-parent"
119
+ /** Merchant-mutable parent code — prohibited on regulated regardless (b/d). */
120
+ | "mutable-parent-prohibited"
121
+ /** No exact content digest (or a malformed one) — a mutable container is ineligible (b). */
122
+ | "unpinned"
123
+ /** The certificate is for a different app/version than the integration under evaluation. */
124
+ | "binding-mismatch"
125
+ /** The served digest differs from the certified pinned digest — drift (c). */
126
+ | "digest-drift"
127
+ /** ext-policy-04 coverage failed (absent / unknown / duplicated / omitted obligation). */
128
+ | "obligation-coverage"
129
+ /** A server-required obligation carries no proof at all (a). */
130
+ | "obligation-unproven"
131
+ /** A required obligation's proof did not withstand its adversarial challenge (a). */
132
+ | "proof-not-adversarial"
133
+ /** A required obligation's proof evidence is not pinned to exact content. */
134
+ | "proof-evidence-unpinned";
135
+
136
+ export interface RegulatedParentCertDenial {
137
+ reason: RegulatedParentCertDenialReason;
138
+ /** The offending value (obligation id, digest, surface, tier, or a coverage message), when one applies. */
139
+ subject?: string;
140
+ }
141
+
142
+ /**
143
+ * The certification decision. `certifiedImmutable` is the single boolean the assurance
144
+ * and capability layers consume: it is `true` ONLY on the `ok` branch, so a regulated
145
+ * elevated-parent integration is enabled exactly when every clause above holds.
146
+ */
147
+ export type RegulatedParentCertDecision =
148
+ | {
149
+ ok: true;
150
+ certifiedImmutable: true;
151
+ certification: RegulatedParentCertification;
152
+ /** The server-derived obligations proven under adversarial challenge (canonical order). */
153
+ provenObligations: string[];
154
+ }
155
+ | { ok: false; certifiedImmutable: false; denials: RegulatedParentCertDenial[] };
156
+
157
+ // A pinned digest is either the runtime's own FNV-1a-128 identity digest (32 lowercase
158
+ // hex chars) or an algorithm-prefixed content hash (e.g. `sha256:<hex>`). A mutable
159
+ // container has neither. Anything else is treated as unpinned and fails closed.
160
+ const FNV128_HEX = /^[0-9a-f]{32}$/;
161
+ const ALGO_DIGEST = /^[a-z][a-z0-9-]*:[0-9a-f]{16,}$/;
162
+
163
+ /** True when a string is a well-formed immutable content digest (never a mutable container). */
164
+ export function isPinnedDigest(digest: string | null | undefined): boolean {
165
+ return typeof digest === "string" && (FNV128_HEX.test(digest) || ALGO_DIGEST.test(digest));
166
+ }
167
+
168
+ /**
169
+ * Compute the runtime's deterministic content digest for an artifact — the value a
170
+ * certifier pins and a mount's served-digest is compared against. Reuses the shared
171
+ * identity primitive (./digest.ts) so a pin and a drift check fold the same bytes.
172
+ */
173
+ export function computeArtifactDigest(content: unknown): string {
174
+ return digestValue(content);
175
+ }
176
+
177
+ /** True when a served artifact has drifted from what was certified (c). */
178
+ export function isCertifiedDigestDrift(
179
+ certification: RegulatedParentCertification,
180
+ servedDigest: string | undefined,
181
+ ): boolean {
182
+ return servedDigest !== undefined && servedDigest !== certification.pinnedDigest;
183
+ }
184
+
185
+ /** True when a single obligation proof is an accepted adversarial, evidence-pinned proof. */
186
+ function isAcceptedProof(proof: ObligationProof | undefined): boolean {
187
+ return (
188
+ !!proof &&
189
+ proof.result === "withstood" &&
190
+ proof.adversarialChallenge.trim().length > 0 &&
191
+ isPinnedDigest(proof.evidenceDigest)
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Decide whether a regulated elevated-parent integration may be enabled. Fail-closed
197
+ * and non-short-circuiting: every clause runs and EVERY reason is accumulated, so the
198
+ * operator sees the complete set of what to fix. `certifiedImmutable` is `true` only
199
+ * when nothing objected — feed it straight into `IntegrationContext` /
200
+ * `AssuranceDecisionContext`.
201
+ *
202
+ * The gate applies to REGULATED tenants only. A non-regulated tenant returns
203
+ * `not-regulated` (this is not its enablement path — see the outside-guarantee posture
204
+ * in ./assurance-decision.ts); it never produces a trivially-true certification.
205
+ */
206
+ export function decideRegulatedParentCert(
207
+ certification: RegulatedParentCertification | null | undefined,
208
+ context: RegulatedParentCertContext,
209
+ ): RegulatedParentCertDecision {
210
+ // The gate is scoped to the regulated surface. Elsewhere, `certifiedImmutable` is
211
+ // not the enablement lever, so this gate declines rather than certifies.
212
+ if (!isRegulated(context.tenant)) {
213
+ return { ok: false, certifiedImmutable: false, denials: [{ reason: "not-regulated" }] };
214
+ }
215
+
216
+ // Disabled by default: absent certification ⇒ disabled (a).
217
+ if (!certification) {
218
+ return { ok: false, certifiedImmutable: false, denials: [{ reason: "certificate-absent" }] };
219
+ }
220
+
221
+ const denials: RegulatedParentCertDenial[] = [];
222
+
223
+ if (certification.model !== REGULATED_PARENT_CERT_MODEL) {
224
+ denials.push({ reason: "unknown-model", subject: String(certification.model) });
225
+ }
226
+ if (certification.surface !== "regulated") {
227
+ denials.push({ reason: "wrong-surface", subject: certification.surface });
228
+ }
229
+ if (certification.executionTier !== "elevated-parent") {
230
+ denials.push({ reason: "not-elevated-parent", subject: certification.executionTier });
231
+ }
232
+
233
+ // (b)/(d) Immutable pinning. Merchant-mutable parent code is never
234
+ // certifiable on regulated — it is rejected, never "certified".
235
+ if (certification.codeForm === "mutable-container") {
236
+ denials.push({ reason: "mutable-parent-prohibited", subject: certification.codeForm });
237
+ } else if (certification.codeForm !== "pinned-immutable") {
238
+ denials.push({ reason: "unpinned", subject: certification.codeForm });
239
+ }
240
+ if (!isPinnedDigest(certification.pinnedDigest)) {
241
+ denials.push({ reason: "unpinned", subject: certification.pinnedDigest || "(absent)" });
242
+ }
243
+
244
+ // The certificate must cover the integration actually being evaluated.
245
+ if (context.binding) {
246
+ if (context.binding.appId !== certification.appId) {
247
+ denials.push({ reason: "binding-mismatch", subject: certification.appId });
248
+ }
249
+ if (context.binding.version !== certification.version) {
250
+ denials.push({ reason: "binding-mismatch", subject: certification.version });
251
+ }
252
+ }
253
+
254
+ // (c) Drift ⇒ auto-disable: the served digest must equal the certified pinned digest.
255
+ if (isCertifiedDigestDrift(certification, context.servedDigest)) {
256
+ denials.push({ reason: "digest-drift", subject: context.servedDigest });
257
+ }
258
+
259
+ // (a) Adversarial per-obligation proof of the SERVER-derived required set. First
260
+ // reuse the ext-policy-04 coverage authority (absent / unknown / duplicated /
261
+ // omitted), then require each required obligation carry a withstood, evidence-pinned
262
+ // proof — a mere present-and-known proof is not enough.
263
+ const proofIds = certification.proofs.map((p) => p.obligationId);
264
+ const coverage = verifyObligationCoverage(context.tenant, proofIds);
265
+ if (!coverage.ok) {
266
+ for (const blocker of coverage.blockers) {
267
+ denials.push({ reason: "obligation-coverage", subject: blocker });
268
+ }
269
+ }
270
+
271
+ const proofById = new Map<string, ObligationProof>();
272
+ for (const proof of certification.proofs) {
273
+ if (!proofById.has(proof.obligationId)) proofById.set(proof.obligationId, proof);
274
+ }
275
+ const required = resolveObligationIds(context.tenant);
276
+ for (const id of required) {
277
+ const proof = proofById.get(id);
278
+ if (!proof) {
279
+ denials.push({ reason: "obligation-unproven", subject: id });
280
+ continue;
281
+ }
282
+ if (proof.result !== "withstood" || proof.adversarialChallenge.trim().length === 0) {
283
+ denials.push({ reason: "proof-not-adversarial", subject: id });
284
+ }
285
+ if (!isPinnedDigest(proof.evidenceDigest)) {
286
+ denials.push({ reason: "proof-evidence-unpinned", subject: id });
287
+ }
288
+ }
289
+
290
+ if (denials.length > 0) {
291
+ return { ok: false, certifiedImmutable: false, denials };
292
+ }
293
+ return { ok: true, certifiedImmutable: true, certification, provenObligations: required };
294
+ }
295
+
296
+ /**
297
+ * True iff a regulated elevated-parent integration is enabled by this certification —
298
+ * the boolean `IntegrationContext.certifiedImmutable` / `AssuranceDecisionContext
299
+ * .certifiedImmutable` expect. Disabled by default: any absent, unpinned, drifted, or
300
+ * incompletely-proven certification yields `false`.
301
+ */
302
+ export function isRegulatedParentCertified(
303
+ certification: RegulatedParentCertification | null | undefined,
304
+ context: RegulatedParentCertContext,
305
+ ): boolean {
306
+ return decideRegulatedParentCert(certification, context).certifiedImmutable;
307
+ }
308
+
309
+ // `isAcceptedProof` is exported for downstream review UIs that render per-proof status
310
+ // without re-deriving the whole decision.
311
+ export { isAcceptedProof };