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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/apps/storefront/astro.config.mjs +3 -3
  2. package/apps/storefront/env.d.ts +54 -5
  3. package/apps/storefront/integrations/materialize-guard.mjs +12 -14
  4. package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
  5. package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
  6. package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
  7. package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
  8. package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
  9. package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
  10. package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
  11. package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
  12. package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
  13. package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
  14. package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
  15. package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
  16. package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
  17. package/apps/storefront/migrations-apps/meta/_journal.json +42 -0
  18. package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
  19. package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
  20. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
  21. package/apps/storefront/public/shared/commerce-chrome.css +20 -2
  22. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
  23. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
  24. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
  25. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
  26. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
  27. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
  28. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
  29. package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
  30. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
  31. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
  32. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
  33. package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
  34. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
  35. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
  36. package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
  37. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
  38. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
  39. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
  40. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
  41. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
  42. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
  43. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
  44. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
  45. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
  46. package/apps/storefront/src/components/Img.astro +1 -1
  47. package/apps/storefront/src/components/admin/AdminBlogTab.astro +19 -16
  48. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
  49. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1057 -412
  50. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  51. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
  52. package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
  53. package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
  54. package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
  55. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
  56. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
  57. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
  58. package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
  59. package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
  60. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
  61. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
  62. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
  63. package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
  64. package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
  65. package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
  66. package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
  67. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
  68. package/apps/storefront/src/components/membership/TierCards.astro +2 -2
  69. package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
  70. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
  71. package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
  72. package/apps/storefront/src/config/devTenantSeed.ts +9 -9
  73. package/apps/storefront/src/config/resolver.ts +4 -4
  74. package/apps/storefront/src/layouts/Layout.astro +100 -20
  75. package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
  76. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +86 -6
  77. package/apps/storefront/src/lib/activity/deployVersion.ts +1 -1
  78. package/apps/storefront/src/lib/admin/adminShell.ts +2 -0
  79. package/apps/storefront/src/lib/admin/envelope.ts +1 -2
  80. package/apps/storefront/src/lib/apps/adminService.ts +10 -0
  81. package/apps/storefront/src/lib/apps/adminSession.ts +1 -1
  82. package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
  83. package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
  84. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
  85. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
  86. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
  87. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
  88. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
  89. package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
  90. package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
  91. package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
  92. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
  93. package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
  94. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +8 -0
  95. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +1 -1
  96. package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
  97. package/apps/storefront/src/lib/apps/registryService.ts +264 -10
  98. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
  99. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
  100. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
  101. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
  102. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
  103. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
  104. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
  105. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
  106. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
  107. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
  108. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
  109. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
  110. package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
  111. package/apps/storefront/src/lib/auth/adminEntry.ts +2 -2
  112. package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
  113. package/apps/storefront/src/lib/blog/collection.ts +97 -10
  114. package/apps/storefront/src/lib/blog/markdown.ts +1 -1
  115. package/apps/storefront/src/lib/blog/provider.ts +57 -3
  116. package/apps/storefront/src/lib/blog/types.ts +9 -9
  117. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
  118. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
  119. package/apps/storefront/src/lib/chrome/model.ts +48 -253
  120. package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
  121. package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
  122. package/apps/storefront/src/lib/chrome/variant.ts +39 -0
  123. package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
  124. package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
  125. package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
  126. package/apps/storefront/src/lib/content/index.ts +5 -5
  127. package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
  128. package/apps/storefront/src/lib/content-edit/route.ts +1 -1
  129. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  130. package/apps/storefront/src/lib/d1/schema-apps.ts +276 -0
  131. package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
  132. package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
  133. package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
  134. package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
  135. package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
  136. package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
  137. package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
  138. package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
  139. package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
  140. package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
  141. package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
  142. package/apps/storefront/src/lib/edgeCache.ts +18 -6
  143. package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
  144. package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
  145. package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
  146. package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
  147. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
  148. package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
  149. package/apps/storefront/src/lib/membership/model.ts +1 -2
  150. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +2 -2
  151. package/apps/storefront/src/lib/newsletter/consent.ts +2 -2
  152. package/apps/storefront/src/lib/newsletter/send.ts +3 -3
  153. package/apps/storefront/src/lib/previewReload.ts +57 -1
  154. package/apps/storefront/src/lib/publish/adminPublishTab.ts +1775 -669
  155. package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
  156. package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
  157. package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
  158. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
  159. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
  160. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
  161. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
  162. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
  163. package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
  164. package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
  165. package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
  166. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
  167. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
  168. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
  169. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
  170. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
  171. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
  172. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
  173. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
  174. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
  175. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
  176. package/apps/storefront/src/lib/publish/shipWorkspace.ts +22 -3
  177. package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
  178. package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
  179. package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
  180. package/apps/storefront/src/lib/rawChrome.ts +125 -47
  181. package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
  182. package/apps/storefront/src/lib/seo/alternates.ts +1 -1
  183. package/apps/storefront/src/lib/social/notify.ts +4 -4
  184. package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
  185. package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
  186. package/apps/storefront/src/lib/subscription/model.ts +4 -4
  187. package/apps/storefront/src/lib/the-build/provider.ts +9 -4
  188. package/apps/storefront/src/middleware/index.ts +273 -51
  189. package/apps/storefront/src/pages/[...slug].astro +9 -4
  190. package/apps/storefront/src/pages/admin/review-release.astro +265 -0
  191. package/apps/storefront/src/pages/admin/select.astro +96 -0
  192. package/apps/storefront/src/pages/admin.astro +16 -2
  193. package/apps/storefront/src/pages/api/activity.ts +8 -8
  194. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
  195. package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
  196. package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
  197. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
  198. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
  199. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
  200. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
  201. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
  202. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
  203. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
  204. package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
  205. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
  206. package/apps/storefront/src/pages/api/rum/vitals.ts +1 -2
  207. package/apps/storefront/src/pages/blog/[slug].astro +5 -5
  208. package/apps/storefront/src/pages/blog/author/[author].astro +3 -3
  209. package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
  210. package/apps/storefront/src/pages/blog/index.astro +2 -2
  211. package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
  212. package/apps/storefront/src/pages/cockpit.astro +61 -1362
  213. package/apps/storefront/src/pages/index.astro +10 -5
  214. package/apps/storefront/src/pages/llms.txt.ts +1 -1
  215. package/apps/storefront/src/pages/products/[handle].astro +7 -6
  216. package/apps/storefront/src/pages/sitemap.xml.ts +2 -2
  217. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
  218. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
  219. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
  220. package/apps/storefront/src/pages/the-build/[slug].astro +4 -4
  221. package/apps/storefront/src/pages/the-build/index.astro +2 -2
  222. package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
  223. package/apps/storefront/src/styles/admin.css +27 -1
  224. package/apps/storefront/tsconfig.json +1 -1
  225. package/package.json +1 -1
  226. package/packages/public-runtime/package.json +2 -1
  227. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
  228. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
  229. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
  230. package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
  231. package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
  232. package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
  233. package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
  234. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
  235. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
  236. package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
  237. package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
  238. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
  239. package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
  240. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
  241. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
  242. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
  243. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
  244. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
  245. package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
  246. package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
  247. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
  248. package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
  249. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
  250. package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
  251. package/packages/public-runtime/src/activity/catalog.ts +41 -37
  252. package/packages/public-runtime/src/activity/event.ts +6 -6
  253. package/packages/public-runtime/src/activity/index.ts +2 -2
  254. package/packages/public-runtime/src/activity/redaction.ts +4 -4
  255. package/packages/public-runtime/src/candidate-index.ts +42 -8
  256. package/packages/public-runtime/src/checkout-orders.ts +2 -2
  257. package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
  258. package/packages/public-runtime/src/checkout-style.ts +7 -7
  259. package/packages/public-runtime/src/checkout.ts +3 -3
  260. package/packages/public-runtime/src/chrome.ts +777 -0
  261. package/packages/public-runtime/src/compliance-obligations.ts +310 -0
  262. package/packages/public-runtime/src/csp.ts +99 -0
  263. package/packages/public-runtime/src/customization-runtime.ts +32 -1
  264. package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
  265. package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
  266. package/packages/public-runtime/src/embed-catalog.mjs +40 -0
  267. package/packages/public-runtime/src/evidence.ts +288 -0
  268. package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
  269. package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
  270. package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
  271. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
  272. package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
  273. package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
  274. package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
  275. package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
  276. package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
  277. package/packages/public-runtime/src/index.ts +16 -1
  278. package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
  279. package/packages/public-runtime/src/integration/assurance.ts +228 -0
  280. package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
  281. package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
  282. package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
  283. package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
  284. package/packages/public-runtime/src/integration/consent-config.ts +260 -0
  285. package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
  286. package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
  287. package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
  288. package/packages/public-runtime/src/integration/csp.ts +105 -0
  289. package/packages/public-runtime/src/integration/digest.ts +43 -0
  290. package/packages/public-runtime/src/integration/environment.ts +228 -0
  291. package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
  292. package/packages/public-runtime/src/integration/events.ts +701 -0
  293. package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
  294. package/packages/public-runtime/src/integration/index.ts +90 -0
  295. package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
  296. package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
  297. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
  298. package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
  299. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
  300. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
  301. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
  302. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
  303. package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
  304. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
  305. package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
  306. package/packages/public-runtime/src/integration/manifest.ts +470 -0
  307. package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
  308. package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
  309. package/packages/public-runtime/src/integration/observability.ts +451 -0
  310. package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
  311. package/packages/public-runtime/src/integration/phases.ts +182 -0
  312. package/packages/public-runtime/src/integration/plan.ts +112 -0
  313. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
  314. package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
  315. package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
  316. package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
  317. package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
  318. package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
  319. package/packages/public-runtime/src/integration/render-phases.ts +267 -0
  320. package/packages/public-runtime/src/integration/resolve.ts +465 -0
  321. package/packages/public-runtime/src/integration/result.ts +239 -0
  322. package/packages/public-runtime/src/integration/secrets.ts +92 -0
  323. package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
  324. package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
  325. package/packages/public-runtime/src/integration/version.ts +92 -0
  326. package/packages/public-runtime/src/integration-capabilities.ts +520 -0
  327. package/packages/public-runtime/src/membership.ts +2 -2
  328. package/packages/public-runtime/src/raw-html-policy.ts +2 -4
  329. package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
  330. package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
  331. package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
  332. package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
  333. package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
  334. package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
  335. package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
  336. package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
  337. package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
  338. package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
  339. package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
  340. package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
  341. package/packages/public-runtime/src/static-bundle.ts +5 -5
  342. package/packages/public-runtime/src/tenant-assets.ts +143 -4
  343. package/packages/public-runtime/src/tenant.ts +45 -9
  344. package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
  345. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
  346. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
  347. package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
  348. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
  349. package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
  350. package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
  351. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
  352. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
  353. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
  354. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
  355. package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
  356. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
  357. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
  358. package/scripts/dev/file-browser.mjs +2 -2
  359. package/scripts/dev/file-writer.mjs +2 -2
  360. package/scripts/dev/integration-guardrails.mjs +498 -0
  361. package/scripts/dev/safe-path.mjs +2 -2
  362. package/scripts/dev/tenant-source-root.mjs +1 -1
  363. package/scripts/tot-dev.mjs +2 -2
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Offline dogfood of the eight v84 nonportable-code dispositions.
3
+ *
4
+ * This module sees only reviewed, redacted dossier metadata. It can emit typed
5
+ * semantic events into an in-memory callback, evaluate a pure path mapping, or
6
+ * record an inert retirement. It has no destination, browser, renderer,
7
+ * persistence, network, install, publish, or source-tenant capability.
8
+ */
9
+ import {
10
+ CONSENT_CONFIG_SCHEMA_VERSION,
11
+ CommerceEventBroker,
12
+ ManagedAnalyticsEventBrokerBridge,
13
+ buildTenantConsentBroker,
14
+ validateConsentConfig,
15
+ validateTenantActionRegistry,
16
+ type CommerceEvent,
17
+ } from "@tot/public-runtime";
18
+ import type {
19
+ V84NonportableCodeIntentDossier,
20
+ } from "./v84NonportableCodeCoaching.js";
21
+ import type { V84CloneMigrationEvidence } from "./v84CloneMigrationEvidence.js";
22
+ import { assertExactV84DogfoodContract } from "./v84NonportableRouteContract.js";
23
+ import {
24
+ sameV84ReplayValue,
25
+ type V84CleanCloneReplayContext,
26
+ } from "./v84CleanCloneReplayContext.js";
27
+
28
+ const SOURCE = "tokenoftrust.com" as const;
29
+ const CLONE = "tokenoftrust.com~clone~gtm-migration-v84" as const;
30
+ const AT = "2026-08-30T00:00:00.000Z";
31
+ const CORRELATION = { id: "corr-v84-nonportable-dogfood", expiresAt: "2026-08-30T02:00:00.000Z" };
32
+ const PROTECTED = ["admin", "account", "identity", "age-assurance", "checkout", "payment"] as const;
33
+
34
+ export type V84DogfoodOutcome = "converted" | "retired" | "evidence-rejected";
35
+
36
+ export interface V84DogfoodLedgerRow {
37
+ readonly identity: string;
38
+ readonly intent: string;
39
+ readonly route: string;
40
+ readonly attemptedSteps: readonly string[];
41
+ readonly artifact: Readonly<Record<string, unknown>>;
42
+ readonly expectedVersusActual: { readonly expected: string; readonly actual: string };
43
+ readonly testsAndCloneQa: readonly string[];
44
+ readonly rollbackProof: string;
45
+ readonly finalOutcome: V84DogfoodOutcome;
46
+ readonly productRunbookGap: string | null;
47
+ }
48
+
49
+ function consentBroker() {
50
+ const checked = validateConsentConfig({
51
+ schemaVersion: CONSENT_CONFIG_SCHEMA_VERSION,
52
+ purposes: [{ purpose: "analytics", legalBasis: "consent", defaultState: "denied" }],
53
+ cmp: { mode: "none" },
54
+ });
55
+ if (!checked.ok) throw new Error("The fixed local consent policy is invalid.");
56
+ const broker = buildTenantConsentBroker(checked.value);
57
+ broker.setDecisions({ analytics: "granted" });
58
+ return broker;
59
+ }
60
+
61
+ function vendorAdapterId(dossier: V84NonportableCodeIntentDossier): string {
62
+ const ids: Readonly<Record<string, string>> = {
63
+ "tag:4": "platform.hubspot",
64
+ "tag:18": "platform.capterra",
65
+ "tag:29": "platform.linkedin",
66
+ "tag:31": "platform.g2",
67
+ };
68
+ const adapterId = ids[dossier.sourceObject.nodeKey];
69
+ if (!adapterId) throw new Error("The vendor destination route is not reviewed.");
70
+ return adapterId;
71
+ }
72
+
73
+ function exerciseNativeHook(dossier: V84NonportableCodeIntentDossier) {
74
+ const catalogLookup = { adapterId: vendorAdapterId(dossier), found: false } as const;
75
+ if (dossier.requiredContract.actionSelection === "owner-selection-required") {
76
+ return {
77
+ kind: "native-contract-refusal",
78
+ hook: dossier.recommendedTotPort.nativeHook,
79
+ approvedChoices: [...dossier.requiredContract.approvedActionNames],
80
+ deliveryCount: 0,
81
+ reason: "owner-action-selection-required",
82
+ catalogLookup,
83
+ } as const;
84
+ }
85
+ const registry = validateTenantActionRegistry({
86
+ schemaVersion: 1,
87
+ actions: dossier.requiredContract.approvedActionNames.map((name) => ({ name })),
88
+ });
89
+ if (!registry.ok) throw new Error("The dossier action contract is invalid.");
90
+ const consent = consentBroker();
91
+ const delivered: CommerceEvent[] = [];
92
+ const broker = new CommerceEventBroker({
93
+ trusted: { tenant: CLONE, environment: "candidate", siteSurface: "marketing" },
94
+ source: `urn:tot:storefront:v84-dogfood:${dossier.dossierId}`,
95
+ consentSnapshot: () => consent.getSnapshot(),
96
+ tenantActionRegistry: registry.value,
97
+ now: () => AT,
98
+ });
99
+ broker.subscribe({
100
+ id: `fake-local-${dossier.dossierId}`,
101
+ consentPurpose: "analytics",
102
+ types: dossier.recommendedTotPort.nativeHook ? [dossier.recommendedTotPort.nativeHook] : [],
103
+ sink: { kind: "callback", handler: (event) => delivered.push(event) },
104
+ });
105
+ const bridge = new ManagedAnalyticsEventBrokerBridge({ broker });
106
+ let results;
107
+ if (dossier.recommendedTotPort.nativeHook === "storefront.action.activated.v1") {
108
+ const initialized = bridge.initialize({ logicalId: `${dossier.dossierId}-document`, path: "/v84-dogfood", correlation: CORRELATION });
109
+ if (!initialized.ok || !initialized.emitted) throw new Error("The local action bridge did not initialize safely.");
110
+ results = dossier.requiredContract.approvedActionNames.map((name, index) =>
111
+ bridge.activate({ logicalId: `${dossier.dossierId}-action-${index + 1}`, name, correlation: CORRELATION }));
112
+ } else {
113
+ results = [bridge.initialize({ logicalId: dossier.dossierId, path: "/v84-dogfood", correlation: CORRELATION })];
114
+ }
115
+ bridge.dispose();
116
+ if (results.some((result) => !result.ok || !result.emitted) || delivered.length !== results.length) {
117
+ throw new Error("The local native-hook exercise did not deliver the exact typed event set.");
118
+ }
119
+ if (delivered.some((event) => event.storefront.tenant !== CLONE || event.storefront.environment === "live")) {
120
+ throw new Error("The local native-hook exercise escaped the diagnostic clone.");
121
+ }
122
+ return {
123
+ kind: "in-memory-native-event-proof",
124
+ hook: delivered[0]?.type,
125
+ payloads: delivered.map((event) => event.data),
126
+ payloadKeys: [...new Set(delivered.flatMap((event) => Object.keys(event.data)))].sort(),
127
+ deliveryCount: delivered.length,
128
+ destination: "fake-local-callback",
129
+ disposed: true,
130
+ catalogLookup,
131
+ } as const;
132
+ }
133
+
134
+ function retire(dossier: V84NonportableCodeIntentDossier, replayContext?: V84CleanCloneReplayContext) {
135
+ if (dossier.recommendedTotPort.nativeHook !== null) {
136
+ throw new Error("A retirement dossier unexpectedly requested runtime authority.");
137
+ }
138
+ const cloneLocalMarkers = new Set<string>();
139
+ const marker = `${CLONE}:retired:${dossier.sourceObject.nodeKey}`;
140
+ const markerKey = `stage/dogfood/retirement/${dossier.sourceObject.nodeKey}`;
141
+ if (replayContext) replayContext.write(markerKey, marker);
142
+ else cloneLocalMarkers.add(marker);
143
+ const markerObserved = replayContext
144
+ ? replayContext.read<string>(markerKey) === marker
145
+ : cloneLocalMarkers.has(marker);
146
+ if (replayContext) replayContext.delete(markerKey);
147
+ else cloneLocalMarkers.delete(marker);
148
+ const markerRemoved = replayContext
149
+ ? replayContext.read<string>(markerKey) === undefined
150
+ : !cloneLocalMarkers.has(marker);
151
+ if (!markerObserved || !markerRemoved) throw new Error("The inert retirement marker was not reversible.");
152
+ return {
153
+ kind: "inert-retirement-record",
154
+ retained: { nodeKey: dossier.sourceObject.nodeKey, dossierId: dossier.dossierId },
155
+ runtimeAuthority: "none",
156
+ installCreated: false,
157
+ markerObserved,
158
+ markerRemoved,
159
+ } as const;
160
+ }
161
+
162
+ function rejectedGap(dossier: V84NonportableCodeIntentDossier): string {
163
+ if (dossier.sourceObject.nodeKey === "variable:38") {
164
+ return "consumer decision required: name the consumer and approve a closed output enum before conversion";
165
+ }
166
+ const gap = dossier.recommendedTotPort.owningProductGap;
167
+ if (!gap) throw new Error("A non-retirement route lacks an exact owning-product gap.");
168
+ const ownerGap = dossier.requiredContract.actionSelection === "owner-selection-required"
169
+ ? "; consumer owner must select one approved action name"
170
+ : "";
171
+ return `owning product capability unavailable: ${gap}${ownerGap}`;
172
+ }
173
+
174
+ function exercise(
175
+ dossier: V84NonportableCodeIntentDossier,
176
+ replayContext?: V84CleanCloneReplayContext,
177
+ ): V84DogfoodLedgerRow {
178
+ if (dossier.recommendedTotPort.route === "retire") {
179
+ return {
180
+ identity: dossier.sourceObject.nodeKey,
181
+ intent: dossier.recoveredBusinessIntent,
182
+ route: "retire",
183
+ attemptedSteps: ["matched the exact redacted identity and graph role", "asserted no native hook", "created and observed a clone-local inert marker", "removed the marker"],
184
+ artifact: retire(dossier, replayContext),
185
+ expectedVersusActual: {
186
+ expected: "retain audit identity with zero runtime behavior",
187
+ actual: "inert retirement recorded; no install, hook, destination, or runtime authority created",
188
+ },
189
+ testsAndCloneQa: ["identity accounted once", "dependency graph checked", "marker observed then removed", "no hook requested", "clone effect counters remain zero"],
190
+ rollbackProof: "No runtime state exists to restore; removing the inert record leaves behavior unchanged.",
191
+ finalOutcome: "retired",
192
+ productRunbookGap: null,
193
+ };
194
+ }
195
+ const isMapping = dossier.recommendedTotPort.route === "declarative-typed-mapping";
196
+ const artifact = isMapping ? {
197
+ kind: "declarative-conversion-refusal",
198
+ observedGraph: { consumer: "trigger:39", variable: "variable:38", consumingTagCount: 0 },
199
+ mappingCreated: false,
200
+ reason: "named-consumer-and-output-enum-unresolved",
201
+ } as const : exerciseNativeHook(dossier);
202
+ const gap = rejectedGap(dossier);
203
+ return {
204
+ identity: dossier.sourceObject.nodeKey,
205
+ intent: dossier.recoveredBusinessIntent,
206
+ route: dossier.recommendedTotPort.route,
207
+ attemptedSteps: isMapping
208
+ ? ["verified trigger:39 references variable:38", "verified no tag consumes trigger:39", "looked for a named consumer and approved output enum", "refused to invent a mapping"]
209
+ : dossier.requiredContract.actionSelection === "owner-selection-required"
210
+ ? ["validated the closed action choice set", "refused to choose for the owner", "confirmed zero delivery", "looked up the owned destination"]
211
+ : ["validated the closed native event contract", "granted local analytics consent", "delivered the exact typed event set to an in-memory callback", "disposed the bridge", "looked up the owned destination"],
212
+ artifact,
213
+ expectedVersusActual: {
214
+ expected: dossier.recommendedTotPort.saferAlternative,
215
+ actual: dossier.requiredContract.actionSelection === "owner-selection-required"
216
+ ? `closed choice set validated with zero emission; exercise stopped: ${gap}`
217
+ : isMapping
218
+ ? `no mapping was created; exercise stopped: ${gap}`
219
+ : `safe local native contract exercise passed, but conversion stopped: ${gap}`,
220
+ },
221
+ testsAndCloneQa: isMapping
222
+ ? ["exact graph edge is present", "consumer tag count is zero", "no mapping or destination effect exists"]
223
+ : dossier.requiredContract.actionSelection === "owner-selection-required"
224
+ ? ["all approved choices retained", "zero events emitted before owner selection", "managed vendor adapter absent"]
225
+ : ["typed payload set delivered exactly once each", "clone tenant and candidate environment pinned", "fake callback disposed", "protected surfaces excluded"],
226
+ rollbackProof: isMapping
227
+ ? "Discarding the refusal artifact leaves the prior no-mapping state unchanged; no consumer was connected."
228
+ : dossier.requiredContract.actionSelection === "owner-selection-required"
229
+ ? "No subscriber, event, install, or destination state was created before the required owner decision."
230
+ : "Disposal removes the only in-memory subscriber; no install or destination state exists.",
231
+ finalOutcome: "evidence-rejected",
232
+ productRunbookGap: gap,
233
+ };
234
+ }
235
+
236
+ /** Exercise every reviewed route and return a terminal eight-row ledger. */
237
+ export function buildV84NonportableRouteDogfood(
238
+ evidence: V84CloneMigrationEvidence,
239
+ catalogAdapterIds: readonly string[],
240
+ replayContext?: V84CleanCloneReplayContext,
241
+ ) {
242
+ if (replayContext && !sameV84ReplayValue(
243
+ replayContext.read<V84CloneMigrationEvidence>("stage/import/handoff"),
244
+ evidence,
245
+ )) {
246
+ throw new Error("The dogfood proof did not receive the shared import handoff.");
247
+ }
248
+ const safe = assertExactV84DogfoodContract(evidence, catalogAdapterIds);
249
+ const coaching = safe.evidence.nonportableCodeCoaching;
250
+ if (!coaching.policy.sourceReadOnly || !coaching.policy.importedArtifactsInert || coaching.policy.installsOrPublishes) {
251
+ throw new Error("The coaching policy is outside the inert diagnostic scope.");
252
+ }
253
+ if (coaching.dossiers.length !== 8 || new Set(coaching.dossiers.map((item) => item.sourceObject.nodeKey)).size !== 8) {
254
+ throw new Error("The dogfood gate requires exactly eight unique dossiers.");
255
+ }
256
+ if (coaching.dossiers.some((item) => item.recommendedTotPort.status !== "not-installed")) {
257
+ throw new Error("The dogfood input must be the reviewed pre-install coaching state.");
258
+ }
259
+ const ledger = coaching.dossiers.map((dossier) => exercise(dossier, replayContext));
260
+ if (ledger.some((row) => !["converted", "retired", "evidence-rejected"].includes(row.finalOutcome))) {
261
+ throw new Error("Every dogfood route must end in an explicit terminal outcome.");
262
+ }
263
+ const result = {
264
+ schemaVersion: 1 as const,
265
+ scope: { sourceTenant: SOURCE, sourceMode: "read-only-fixture" as const, cloneTenant: CLONE, destinationMode: "fake-local-only" as const },
266
+ ledger,
267
+ summary: {
268
+ total: ledger.length,
269
+ converted: ledger.filter((row) => row.finalOutcome === "converted").length,
270
+ retired: ledger.filter((row) => row.finalOutcome === "retired").length,
271
+ evidenceRejected: ledger.filter((row) => row.finalOutcome === "evidence-rejected").length,
272
+ unexplainedNotInstalled: 0,
273
+ },
274
+ catalogLookup: {
275
+ presentAdapterIds: [...safe.catalogAdapterIds].sort(),
276
+ requestedVendorAdapterIds: ["platform.hubspot", "platform.capterra", "platform.linkedin", "platform.g2"],
277
+ missingVendorAdapterIds: ["platform.hubspot", "platform.capterra", "platform.linkedin", "platform.g2"],
278
+ },
279
+ safety: {
280
+ sourceReads: 0, sourceWrites: 0, clonePersistentWrites: 0, externalNetworkRequests: 0,
281
+ liveSecretsUsed: 0, productionActions: 0, importedCodeExecuted: false,
282
+ originalMarkupRendered: false, runtimeLoaderInitialized: false, dataLayerTouched: false,
283
+ },
284
+ protectedSurfaces: [...PROTECTED],
285
+ };
286
+ if (replayContext) {
287
+ for (const row of ledger) replayContext.write(`stage/dogfood/artifacts/${row.identity}`, row.artifact);
288
+ replayContext.write("stage/dogfood/handoff", result);
289
+ }
290
+ return result;
291
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The EDGE transport for the pinned managed-analytics v1 contract: a thin HTTP
3
+ * adapter that satisfies {@link Ws1ManagedAnalyticsClient} by POSTing each
4
+ * pinned request to WS1's lifecycle service and returning the pinned response
5
+ * shape. WS1 owns the actual service; this file only carries the request over
6
+ * the wire.
7
+ *
8
+ * Tenant authority is server-derived, exactly as the pinned contract demands
9
+ * ("no request in this module accepts a tenant id"): the tenant is sent as a
10
+ * server-to-server header alongside the service credential — NEVER in a request
11
+ * body, and NEVER read from the caller's request body. Unconfigured service URL
12
+ * -> `null` (the route answers 503), never a guess.
13
+ */
14
+ import type { APIContext } from "astro";
15
+ import {
16
+ MANAGED_ANALYTICS_API_V1_VERSION_POLICY,
17
+ type CreateMigrationDraftRequest,
18
+ type CreateMigrationDraftResponse,
19
+ } from "@tot/public-runtime";
20
+ import { readEnv } from "../env.js";
21
+ import type { Ws1ManagedAnalyticsClient } from "./gtmMigrationInstall.js";
22
+
23
+ export interface Ws1ManagedAnalyticsMigrationClient extends Ws1ManagedAnalyticsClient {
24
+ createMigrationDraft(request: CreateMigrationDraftRequest): Promise<CreateMigrationDraftResponse>;
25
+ }
26
+
27
+ export type Ws1Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
28
+
29
+ /** Base URL of WS1's managed-analytics lifecycle service (scheme + host). */
30
+ const SERVICE_URL_ENV = "MANAGED_ANALYTICS_API_URL";
31
+ /** Server-to-server bearer credential for WS1 (scopes the tenant server-side). */
32
+ const SERVICE_SECRET_ENV = "MANAGED_ANALYTICS_API_SECRET";
33
+
34
+ /** The versioned base path, taken from the pinned version policy — never hard-coded twice. */
35
+ const V1_PATH = MANAGED_ANALYTICS_API_V1_VERSION_POLICY.path;
36
+
37
+ /** POST one pinned request to `<base><v1>/<operation>` and return the parsed pinned response. */
38
+ function post<TReq, TRes>(
39
+ fetchImpl: Ws1Fetch,
40
+ baseUrl: string,
41
+ operation: string,
42
+ headers: Record<string, string>,
43
+ ): (request: TReq) => Promise<TRes> {
44
+ return async (request: TReq): Promise<TRes> => {
45
+ const res = await fetchImpl(`${baseUrl}${V1_PATH}/${operation}`, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json", ...headers },
48
+ body: JSON.stringify(request),
49
+ });
50
+ // WS1 returns the pinned success OR error envelope as JSON for every status.
51
+ return (await res.json()) as TRes;
52
+ };
53
+ }
54
+
55
+ /** Build the tenant-bound public WS1 HTTP client; tests may inject a no-network fetch. */
56
+ export function createWs1ManagedAnalyticsClient(input: {
57
+ readonly baseUrl: string;
58
+ readonly tenantId: string;
59
+ readonly authorization?: string;
60
+ readonly fetchImpl?: Ws1Fetch;
61
+ }): Ws1ManagedAnalyticsMigrationClient {
62
+ const baseUrl = input.baseUrl.replace(/\/$/, "");
63
+ const headers: Record<string, string> = { "x-tot-tenant": input.tenantId };
64
+ if (input.authorization) headers.authorization = input.authorization;
65
+ const fetchImpl = input.fetchImpl ?? fetch;
66
+ return {
67
+ getMigrationDraft: post(fetchImpl, baseUrl, "getMigrationDraft", headers),
68
+ createMigrationDraft: post(fetchImpl, baseUrl, "createMigrationDraft", headers),
69
+ getAdapterDefinition: post(fetchImpl, baseUrl, "getAdapterDefinition", headers),
70
+ getIntegrationInstall: post(fetchImpl, baseUrl, "getIntegrationInstall", headers),
71
+ createIntegrationInstall: post(fetchImpl, baseUrl, "createIntegrationInstall", headers),
72
+ reviseIntegrationInstall: post(fetchImpl, baseUrl, "reviseIntegrationInstall", headers),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Build an HTTP-backed {@link Ws1ManagedAnalyticsClient} for `tenantId`, or
78
+ * `null` when WS1's service URL isn't configured in this runtime. `tenantId`
79
+ * comes from the resolved operator session — never the request body — and is
80
+ * sent as a server-to-server header, keeping every request body tenant-free per
81
+ * the pinned contract.
82
+ */
83
+ export async function resolveWs1ManagedAnalyticsClient(
84
+ _context: APIContext,
85
+ tenantId: string,
86
+ ): Promise<Ws1ManagedAnalyticsClient | null> {
87
+ const baseUrl = (await readEnv(SERVICE_URL_ENV))?.replace(/\/$/, "");
88
+ if (!baseUrl) return null;
89
+ const secret = await readEnv(SERVICE_SECRET_ENV);
90
+ return createWs1ManagedAnalyticsClient({
91
+ baseUrl,
92
+ tenantId,
93
+ ...(secret ? { authorization: `Bearer ${secret}` } : {}),
94
+ });
95
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Fail-closed access guard for tenant-scoped admin APIs (`/api/admin/**`).
3
+ *
4
+ * The admin JSON routes must NOT trust the ambient login gate for their auth:
5
+ * that gate is conditional (`DEMO_GATE_MODE="login"` on a gated host), so a route
6
+ * that leans on it is wide open on any non-gated host. This guard makes each
7
+ * admin API validate the caller EXPLICITLY, bound to the Host-resolved tenant
8
+ * (`locals.tenant.appDomain` — never a client value, never a default), and
9
+ * deny-closed on absence:
10
+ *
11
+ * no session → 401 sign_in_required
12
+ * session, but no standing for THIS tenant → 403 not_authorized
13
+ * member/owner (or staff) of THIS tenant → allowed, entry returned
14
+ *
15
+ * The decision core is PURE (unit-testable); `requireAdminApiEntry` is the thin
16
+ * Astro/I-O wrapper the routes call.
17
+ */
18
+ import type { APIContext } from "astro";
19
+ import { readViewerSession } from "@/lib/auth/route";
20
+ import { resolveAdminEntry, type AdminEntryDecision } from "@/lib/auth/adminEntry";
21
+ import type { SessionRecord } from "./session.js";
22
+
23
+ export type AdminApiAccess =
24
+ | { ok: true; entry: AdminEntryDecision }
25
+ | { ok: false; status: 401 | 403; reason: "sign_in_required" | "not_authorized" };
26
+
27
+ /**
28
+ * PURE fail-closed decision for a tenant-scoped admin API caller.
29
+ *
30
+ * @param requireOwner when true, only an owner/admin viewer OR a ToT-staff viewer
31
+ * passes (a plain team member is refused) — for routes that mutate/expose
32
+ * owner-level operational surfaces. Default (false) admits any member/owner/staff
33
+ * of the tenant, matching who reaches the admin shell.
34
+ */
35
+ export function decideAdminApiAccess(input: {
36
+ record: SessionRecord | null | undefined;
37
+ hostResource: string;
38
+ nowSeconds: number;
39
+ requireOwner?: boolean;
40
+ }): AdminApiAccess {
41
+ const { record, hostResource, nowSeconds, requireOwner } = input;
42
+ if (!record) return { ok: false, status: 401, reason: "sign_in_required" };
43
+
44
+ const entry = resolveAdminEntry({ record, hostResource, nowSeconds });
45
+ const staffScope = Array.isArray(record.staff) && record.staff.length > 0;
46
+ const authorized = requireOwner
47
+ ? entry.isOwnerViewer || staffScope
48
+ : entry.admitted;
49
+ if (!authorized) return { ok: false, status: 403, reason: "not_authorized" };
50
+
51
+ return { ok: true, entry };
52
+ }
53
+
54
+ /** Astro wrapper: read the viewer session and decide, bound to the routed tenant. */
55
+ export async function requireAdminApiEntry(
56
+ context: APIContext,
57
+ opts?: { requireOwner?: boolean },
58
+ ): Promise<AdminApiAccess> {
59
+ const record = await readViewerSession(context);
60
+ return decideAdminApiAccess({
61
+ record,
62
+ hostResource: context.locals.tenant?.appDomain ?? "",
63
+ nowSeconds: Math.floor(Date.now() / 1000),
64
+ requireOwner: opts?.requireOwner,
65
+ });
66
+ }
@@ -30,11 +30,11 @@
30
30
  */
31
31
  import type { SessionRecord } from "./session.js";
32
32
  import { sessionHostCapability } from "./loginGate.js";
33
- import { decideIsOwner } from "@/lib/change/route";
33
+ import { decideIsOwner } from "@/lib/change/route/route";
34
34
  import {
35
35
  resolveShipCapabilities,
36
36
  type ShipCapabilities,
37
- } from "@/lib/change/shipCapabilities";
37
+ } from "@/lib/change/route/ship-capabilities";
38
38
  import type { Capability } from "@/lib/change/types";
39
39
  import { OWNER_CAPABILITIES } from "@/lib/grants/session";
40
40
 
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Canonical admin tenant-landing decision (pure).
3
+ *
4
+ * The admin console is Host-routed: each tenant's admin lives at that tenant's
5
+ * host (or the DNS-free `/<tenant>/admin` path form). "Which tenant" is therefore
6
+ * carried by the URL, never inferred from an ambient global. The ambiguity this
7
+ * module resolves is the ONE case the URL alone can't settle: an authenticated
8
+ * viewer who lands on the admin of a tenant they are NOT a member of — because
9
+ * they followed a stale/shared deep link, or the host fell back to the registry
10
+ * default tenant. The wrong answers are (a) bounce them to /auth/login even though
11
+ * they hold a valid session (a sign-in loop), or (b) silently show them the wrong
12
+ * tenant's data. The canonical answer:
13
+ *
14
+ * - member/owner/staff of the routed tenant → ENTER (render admin here).
15
+ * - no session → LOGIN (gate, `next` preserved).
16
+ * - authed, not here, exactly ONE tenant → AUTO (their sole tenant's /admin).
17
+ * - authed, not here, zero OR many tenants → SELECT (the /admin/select picker;
18
+ * the page differentiates 0 vs many).
19
+ *
20
+ * PURE (no I/O, no Astro): it COMPOSES `sessionAdmitsHost` (the host-bound
21
+ * admission the middleware gate already uses) and `selectTenant` (the vendor
22
+ * single-vs-picker rule) so the console, the gate, and this landing can never
23
+ * drift on "does this viewer belong to this tenant". Unit-testable in isolation.
24
+ */
25
+ import type { SessionRecord } from "./session.js";
26
+ import { sessionAdmitsHost } from "./loginGate.js";
27
+ import { safeNext } from "./route.js";
28
+ import { selectTenant } from "@/lib/dashboard/tenantSelection";
29
+
30
+ /** The tenant-selection surface, in both host-routed and tenant-first path forms. */
31
+ export function isAdminSelectPath(pathname: string): boolean {
32
+ if (pathname === "/admin/select") return true;
33
+ return /^\/[^/]+\/admin\/select$/.test(pathname);
34
+ }
35
+
36
+ /**
37
+ * The DNS-free path-form URL of a tenant's admin on the current host. A leading
38
+ * `/<resource>/` segment middleware-selects that tenant regardless of which host
39
+ * the request arrived on, so this reaches ANOTHER tenant's admin from here.
40
+ */
41
+ export function adminUrlForTenant(resource: string): string {
42
+ return `/${resource.trim()}/admin`;
43
+ }
44
+
45
+ export type AdminLanding =
46
+ | { action: "enter" }
47
+ | { action: "login"; location: string }
48
+ | { action: "auto"; location: string }
49
+ | { action: "select"; location: string };
50
+
51
+ export interface AdminLandingInput {
52
+ /** The viewer's full session (all memberships), or null when unauthenticated. */
53
+ record: SessionRecord | null | undefined;
54
+ /** The Host-resolved tenant's appDomain (server-derived, never client input). */
55
+ routedTenant: string;
56
+ /** locals.basePath: "" (host-form) or "/<domain>" (tenant-first path form). */
57
+ basePath: string;
58
+ /** The intended admin destination (`pathname + search`), preserved as `next`. */
59
+ intended: string;
60
+ /** Epoch seconds, for the staff-selection expiry check. */
61
+ nowSeconds: number;
62
+ }
63
+
64
+ export function resolveAdminLanding(input: AdminLandingInput): AdminLanding {
65
+ const { record, routedTenant, basePath, intended, nowSeconds } = input;
66
+
67
+ if (!record) {
68
+ const next = encodeURIComponent(safeNext(intended));
69
+ return { action: "login", location: `/auth/login?next=${next}` };
70
+ }
71
+
72
+ // Member/owner of the routed tenant, or a ToT-staff viewer → this IS their
73
+ // admin context. Identical admission to the middleware gate, by construction.
74
+ if (sessionAdmitsHost(record, routedTenant, nowSeconds)) {
75
+ return { action: "enter" };
76
+ }
77
+
78
+ // Authenticated, but with no standing for the routed tenant. Route them to
79
+ // their OWN tenant(s) rather than the wrong one or a sign-in loop. A sole
80
+ // membership auto-selects; zero or several go to the picker (which honestly
81
+ // renders "nothing to manage" for zero).
82
+ const selection = selectTenant(record.tenants);
83
+ const selectLocation = `${basePath || ""}/admin/select`;
84
+ if (selection.mode === "single") {
85
+ return { action: "auto", location: adminUrlForTenant(selection.tenant.resource) };
86
+ }
87
+ return { action: "select", location: selectLocation };
88
+ }