@tokenoftrust/storefront-runner 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/apps/storefront/astro.config.mjs +17 -1
  2. package/apps/storefront/env.d.ts +54 -5
  3. package/apps/storefront/integrations/materialize-guard.mjs +54 -0
  4. package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
  5. package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
  6. package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
  7. package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
  8. package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
  9. package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
  10. package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
  11. package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
  12. package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
  13. package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
  14. package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
  15. package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
  16. package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
  17. package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
  18. package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
  19. package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
  20. package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
  21. package/apps/storefront/migrations-apps/meta/_journal.json +56 -0
  22. package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
  23. package/apps/storefront/public/js/dashboard-team.js +38 -2
  24. package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
  25. package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
  26. package/apps/storefront/public/shared/commerce-chrome.css +20 -2
  27. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
  28. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
  29. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
  30. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
  31. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
  32. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
  33. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
  34. package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
  35. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
  36. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
  37. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
  38. package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
  39. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
  40. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
  41. package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
  42. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
  43. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
  44. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
  45. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
  46. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
  47. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
  48. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
  49. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
  50. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
  51. package/apps/storefront/src/components/Img.astro +1 -1
  52. package/apps/storefront/src/components/Seo.astro +65 -1
  53. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
  54. package/apps/storefront/src/components/admin/AdminBlogTab.astro +1435 -0
  55. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
  56. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
  57. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1980 -2564
  58. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  59. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
  60. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
  61. package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
  62. package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
  63. package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
  64. package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
  65. package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
  66. package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
  67. package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
  68. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
  69. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
  70. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
  71. package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
  72. package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
  73. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
  74. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
  75. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
  76. package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
  77. package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
  78. package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
  79. package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
  80. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
  81. package/apps/storefront/src/components/membership/TierCards.astro +2 -2
  82. package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
  83. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
  84. package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
  85. package/apps/storefront/src/config/devTenantSeed.ts +9 -9
  86. package/apps/storefront/src/config/resolver.ts +4 -4
  87. package/apps/storefront/src/layouts/Layout.astro +146 -21
  88. package/apps/storefront/src/lib/activity/alerts.ts +17 -19
  89. package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
  90. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +89 -9
  91. package/apps/storefront/src/lib/activity/deployVersion.ts +20 -27
  92. package/apps/storefront/src/lib/activity/ingest.ts +8 -8
  93. package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
  94. package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
  95. package/apps/storefront/src/lib/activity/query.ts +9 -9
  96. package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
  97. package/apps/storefront/src/lib/activity/store.ts +4 -6
  98. package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
  99. package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
  100. package/apps/storefront/src/lib/admin/adminShell.ts +292 -0
  101. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
  102. package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
  103. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
  104. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
  105. package/apps/storefront/src/lib/admin/envelope.ts +1 -2
  106. package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
  107. package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
  108. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
  109. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
  110. package/apps/storefront/src/lib/apps/adminService.ts +14 -4
  111. package/apps/storefront/src/lib/apps/adminSession.ts +54 -45
  112. package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
  113. package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
  114. package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
  115. package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
  116. package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
  117. package/apps/storefront/src/lib/apps/credentials.ts +2 -2
  118. package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
  119. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
  120. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
  121. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
  122. package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
  123. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
  124. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
  125. package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
  126. package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
  127. package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
  128. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
  129. package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
  130. package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
  131. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
  132. package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
  133. package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
  134. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +9 -1
  135. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
  136. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +7 -7
  137. package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
  138. package/apps/storefront/src/lib/apps/registryService.ts +270 -16
  139. package/apps/storefront/src/lib/apps/scopes.ts +2 -2
  140. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
  141. package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
  142. package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
  143. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
  144. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
  145. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
  146. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
  147. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
  148. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
  149. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
  150. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
  151. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
  152. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
  153. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
  154. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
  155. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
  156. package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
  157. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
  158. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
  159. package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
  160. package/apps/storefront/src/lib/auth/adminEntry.ts +11 -11
  161. package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
  162. package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
  163. package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
  164. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
  165. package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
  166. package/apps/storefront/src/lib/auth/session.ts +7 -8
  167. package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
  168. package/apps/storefront/src/lib/basePath.ts +9 -2
  169. package/apps/storefront/src/lib/blog/access.ts +29 -0
  170. package/apps/storefront/src/lib/blog/collection.ts +400 -0
  171. package/apps/storefront/src/lib/blog/markdown.ts +174 -0
  172. package/apps/storefront/src/lib/blog/provider.ts +62 -3
  173. package/apps/storefront/src/lib/blog/reactions.ts +87 -0
  174. package/apps/storefront/src/lib/blog/rss.ts +92 -0
  175. package/apps/storefront/src/lib/blog/teaser.ts +83 -0
  176. package/apps/storefront/src/lib/blog/types.ts +34 -0
  177. package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
  178. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
  179. package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
  180. package/apps/storefront/src/lib/chrome/model.ts +48 -253
  181. package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
  182. package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
  183. package/apps/storefront/src/lib/chrome/variant.ts +39 -0
  184. package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
  185. package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
  186. package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
  187. package/apps/storefront/src/lib/content/index.ts +5 -5
  188. package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
  189. package/apps/storefront/src/lib/content-edit/route.ts +1 -1
  190. package/apps/storefront/src/lib/d1/catalog.ts +14 -2
  191. package/apps/storefront/src/lib/d1/schema-apps.ts +372 -17
  192. package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
  193. package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
  194. package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
  195. package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
  196. package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
  197. package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
  198. package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
  199. package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
  200. package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
  201. package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
  202. package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
  203. package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
  204. package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
  205. package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
  206. package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
  207. package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
  208. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
  209. package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
  210. package/apps/storefront/src/lib/edgeCache.ts +34 -5
  211. package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
  212. package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
  213. package/apps/storefront/src/lib/i18n.ts +25 -0
  214. package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
  215. package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
  216. package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
  217. package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
  218. package/apps/storefront/src/lib/jsonld.ts +118 -0
  219. package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
  220. package/apps/storefront/src/lib/membership/model.ts +1 -2
  221. package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
  222. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
  223. package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
  224. package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
  225. package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
  226. package/apps/storefront/src/lib/newsletter/send.ts +265 -0
  227. package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
  228. package/apps/storefront/src/lib/previewReload.ts +57 -1
  229. package/apps/storefront/src/lib/publish/adminPublishTab.ts +4625 -0
  230. package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
  231. package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
  232. package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
  233. package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
  234. package/apps/storefront/src/lib/publish/domainState.ts +22 -13
  235. package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
  236. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
  237. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
  238. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
  239. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
  240. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
  241. package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
  242. package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
  243. package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
  244. package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
  245. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
  246. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
  247. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
  248. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
  249. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
  250. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
  251. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
  252. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
  253. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
  254. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
  255. package/apps/storefront/src/lib/publish/shipWorkspace.ts +121 -51
  256. package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
  257. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
  258. package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
  259. package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
  260. package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
  261. package/apps/storefront/src/lib/rawChrome.ts +125 -47
  262. package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
  263. package/apps/storefront/src/lib/search/index.ts +2 -2
  264. package/apps/storefront/src/lib/seo/alternates.ts +42 -0
  265. package/apps/storefront/src/lib/seo/meta.ts +65 -0
  266. package/apps/storefront/src/lib/social/golive.ts +212 -0
  267. package/apps/storefront/src/lib/social/notify.ts +32 -0
  268. package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
  269. package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
  270. package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
  271. package/apps/storefront/src/lib/subscription/model.ts +4 -4
  272. package/apps/storefront/src/lib/the-build/provider.ts +32 -0
  273. package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
  274. package/apps/storefront/src/lib/the-build/toc.ts +95 -0
  275. package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
  276. package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
  277. package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
  278. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
  279. package/apps/storefront/src/middleware/index.ts +385 -52
  280. package/apps/storefront/src/pages/[...slug].astro +9 -4
  281. package/apps/storefront/src/pages/admin/review-release.astro +265 -0
  282. package/apps/storefront/src/pages/admin/select.astro +96 -0
  283. package/apps/storefront/src/pages/admin.astro +25 -1313
  284. package/apps/storefront/src/pages/api/activity.ts +8 -8
  285. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
  286. package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
  287. package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
  288. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
  289. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
  290. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
  291. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
  292. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
  293. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
  294. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
  295. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
  296. package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
  297. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
  298. package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
  299. package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
  300. package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
  301. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
  302. package/apps/storefront/src/pages/api/rum/vitals.ts +20 -2
  303. package/apps/storefront/src/pages/blog/[slug].astro +99 -19
  304. package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
  305. package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
  306. package/apps/storefront/src/pages/blog/index.astro +4 -2
  307. package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
  308. package/apps/storefront/src/pages/cockpit.astro +77 -1353
  309. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
  310. package/apps/storefront/src/pages/index.astro +10 -5
  311. package/apps/storefront/src/pages/llms.txt.ts +1 -1
  312. package/apps/storefront/src/pages/products/[handle].astro +7 -6
  313. package/apps/storefront/src/pages/search.astro +1 -1
  314. package/apps/storefront/src/pages/sitemap.xml.ts +21 -1
  315. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
  316. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
  317. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
  318. package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
  319. package/apps/storefront/src/pages/the-build/index.astro +146 -0
  320. package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
  321. package/apps/storefront/src/styles/admin.css +1106 -0
  322. package/apps/storefront/tsconfig.json +6 -1
  323. package/package.json +1 -1
  324. package/packages/public-runtime/package.json +3 -1
  325. package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
  326. package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
  327. package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
  328. package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
  329. package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
  330. package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
  331. package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
  332. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
  333. package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
  334. package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
  335. package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
  336. package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
  337. package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
  338. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
  339. package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
  340. package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
  341. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
  342. package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
  343. package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
  344. package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
  345. package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
  346. package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
  347. package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
  348. package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
  349. package/packages/public-runtime/src/activity/catalog.ts +89 -33
  350. package/packages/public-runtime/src/activity/event.ts +6 -6
  351. package/packages/public-runtime/src/activity/index.ts +2 -2
  352. package/packages/public-runtime/src/activity/redaction.ts +4 -4
  353. package/packages/public-runtime/src/candidate-index.ts +42 -8
  354. package/packages/public-runtime/src/catalog-d1.ts +1 -1
  355. package/packages/public-runtime/src/checkout-orders.ts +2 -2
  356. package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
  357. package/packages/public-runtime/src/checkout-style.ts +7 -7
  358. package/packages/public-runtime/src/checkout.ts +3 -3
  359. package/packages/public-runtime/src/chrome.ts +777 -0
  360. package/packages/public-runtime/src/compliance-obligations.ts +310 -0
  361. package/packages/public-runtime/src/csp.ts +99 -0
  362. package/packages/public-runtime/src/customization-preview.ts +25 -2
  363. package/packages/public-runtime/src/customization-runtime.ts +156 -3
  364. package/packages/public-runtime/src/customization-versioning.ts +10 -0
  365. package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
  366. package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
  367. package/packages/public-runtime/src/embed-catalog.mjs +40 -0
  368. package/packages/public-runtime/src/evidence.ts +288 -0
  369. package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
  370. package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
  371. package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
  372. package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
  373. package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
  374. package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
  375. package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
  376. package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
  377. package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
  378. package/packages/public-runtime/src/index.ts +17 -1
  379. package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
  380. package/packages/public-runtime/src/integration/assurance.ts +228 -0
  381. package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
  382. package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
  383. package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
  384. package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
  385. package/packages/public-runtime/src/integration/consent-config.ts +260 -0
  386. package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
  387. package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
  388. package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
  389. package/packages/public-runtime/src/integration/csp.ts +105 -0
  390. package/packages/public-runtime/src/integration/digest.ts +43 -0
  391. package/packages/public-runtime/src/integration/environment.ts +228 -0
  392. package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
  393. package/packages/public-runtime/src/integration/events.ts +701 -0
  394. package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
  395. package/packages/public-runtime/src/integration/index.ts +90 -0
  396. package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
  397. package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
  398. package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
  399. package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
  400. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
  401. package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
  402. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
  403. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
  404. package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
  405. package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
  406. package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
  407. package/packages/public-runtime/src/integration/manifest.ts +470 -0
  408. package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
  409. package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
  410. package/packages/public-runtime/src/integration/observability.ts +451 -0
  411. package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
  412. package/packages/public-runtime/src/integration/phases.ts +182 -0
  413. package/packages/public-runtime/src/integration/plan.ts +112 -0
  414. package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
  415. package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
  416. package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
  417. package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
  418. package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
  419. package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
  420. package/packages/public-runtime/src/integration/render-phases.ts +267 -0
  421. package/packages/public-runtime/src/integration/resolve.ts +465 -0
  422. package/packages/public-runtime/src/integration/result.ts +239 -0
  423. package/packages/public-runtime/src/integration/secrets.ts +92 -0
  424. package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
  425. package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
  426. package/packages/public-runtime/src/integration/version.ts +92 -0
  427. package/packages/public-runtime/src/integration-capabilities.ts +520 -0
  428. package/packages/public-runtime/src/membership.ts +2 -2
  429. package/packages/public-runtime/src/raw-html-policy.ts +2 -4
  430. package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
  431. package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
  432. package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
  433. package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
  434. package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
  435. package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
  436. package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
  437. package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
  438. package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
  439. package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
  440. package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
  441. package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
  442. package/packages/public-runtime/src/static-bundle.ts +357 -0
  443. package/packages/public-runtime/src/tenant-assets.ts +143 -4
  444. package/packages/public-runtime/src/tenant.ts +81 -17
  445. package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
  446. package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
  447. package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
  448. package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
  449. package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
  450. package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
  451. package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
  452. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
  453. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
  454. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
  455. package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
  456. package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
  457. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
  458. package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
  459. package/scripts/build/copy-tenant-assets.mjs +14 -4
  460. package/scripts/dev/ai-edit.mjs +1 -1
  461. package/scripts/dev/checkout-watch.mjs +2 -2
  462. package/scripts/dev/file-browser.mjs +4 -6
  463. package/scripts/dev/file-writer.mjs +3 -3
  464. package/scripts/dev/git-status.mjs +2 -2
  465. package/scripts/dev/integration-guardrails.mjs +498 -0
  466. package/scripts/dev/locate-handler.mjs +3 -3
  467. package/scripts/dev/port-check.mjs +3 -3
  468. package/scripts/dev/publish.mjs +5 -9
  469. package/scripts/dev/safe-path.mjs +2 -2
  470. package/scripts/dev/shot.mjs +1 -1
  471. package/scripts/dev/tenant-source-root.mjs +1 -1
  472. package/scripts/dev/unified-diff.mjs +8 -3
  473. package/scripts/tot-dev.mjs +8 -11
  474. package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
@@ -0,0 +1,400 @@
1
+ import type { AuthorBio, BlogPost, BlogPostSummary, PostStatus } from "./types.js";
2
+ import type { ContentProvider } from "../storyblok/provider.js";
3
+
4
+ type ArtifactSource = Pick<ContentProvider, "readArtifact" | "listArtifactPaths">;
5
+ function canReadArtifacts(value: ArtifactSource | undefined): value is ArtifactSource {
6
+ return typeof value?.listArtifactPaths === "function" && typeof value.readArtifact === "function";
7
+ }
8
+ export interface CollectionReadOptions {
9
+ includeUnpublished?: boolean;
10
+ now?: string;
11
+ content?: ArtifactSource;
12
+ }
13
+
14
+ /** A blog collection = a route base + a per-tenant content dir. */
15
+ export interface Collection {
16
+ id: string;
17
+ base: string;
18
+ }
19
+
20
+ // ONE glob covering every tenant + every collection; the tenant id and the
21
+ // collection id are both path segments filtered at call time. The derived index
22
+ // is the set of `posts/*.json` files — there is no hand-maintained index.json.
23
+ const postGlobs = import.meta.glob<BlogPost>(
24
+ "../../../../../tenants/*/content/*/posts/*.json",
25
+ { import: "default" },
26
+ );
27
+ const redirectGlobs = import.meta.glob<Record<string, string>>(
28
+ "../../../../../tenants/*/content/*/redirects.json",
29
+ { import: "default" },
30
+ );
31
+ const authorsGlobs = import.meta.glob<Record<string, AuthorBio>>(
32
+ "../../../../../tenants/*/content/*/authors.json",
33
+ { import: "default" },
34
+ );
35
+
36
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
37
+ const SEGMENT_RE = /^[a-z0-9][a-z0-9._-]*$/;
38
+
39
+ /**
40
+ * A "scheduled" post whose `scheduledAt` instant has passed behaves exactly like
41
+ * "published". Before that instant it is treated like a draft (invisible, unlisted).
42
+ * A scheduled post with a missing/unparseable `scheduledAt` is never due (safe: it
43
+ * stays hidden rather than leaking). Because visibility is computed at build/request
44
+ * time, a post committed ahead of time simply needs a build after its instant.
45
+ */
46
+ function isDueScheduled(p: { status: PostStatus; scheduledAt?: string }, now: string): boolean {
47
+ if (p.status !== "scheduled" || typeof p.scheduledAt !== "string") return false;
48
+ const due = Date.parse(p.scheduledAt);
49
+ return !Number.isNaN(due) && due <= Date.parse(now);
50
+ }
51
+
52
+ /**
53
+ * The single visibility chokepoint: is a post reachable at its URL on the live
54
+ * site? Published, unlisted, and due-scheduled resolve; drafts and not-yet-due
55
+ * scheduled posts 404. Listings (index/RSS/sitemap) apply a stricter filter
56
+ * (published or due-scheduled) — unlisted posts resolve at their URL but never
57
+ * appear in any listing. ("scheduled" branch added in B3.1.)
58
+ */
59
+ export function isVisibleOnLive(
60
+ p: { status: PostStatus; scheduledAt?: string },
61
+ now: string = new Date().toISOString(),
62
+ ): boolean {
63
+ return p.status === "published" || p.status === "unlisted" || isDueScheduled(p, now);
64
+ }
65
+
66
+ /** "Category Insurance" -> "category-insurance" */
67
+ export function categorySlug(name: string): string {
68
+ return name
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .replace(/^-+|-+$/g, "");
72
+ }
73
+
74
+ /**
75
+ * Collect the posts whose `field` (categories or tags) contains a term slugifying
76
+ * to `slug`, preserving input order, plus the human label of the first matching
77
+ * term (for the page heading/breadcrumb). Returns null when nothing matches, which
78
+ * a category/tag route turns into a 404. Terms are matched by `categorySlug` so the
79
+ * URL segment is stable regardless of the term's display casing/spacing.
80
+ */
81
+ export function collectPostsByTerm<T extends BlogPostSummary>(
82
+ posts: T[],
83
+ field: "categories" | "tags",
84
+ slug: string,
85
+ ): { label: string; posts: T[] } | null {
86
+ const matched: T[] = [];
87
+ let label: string | null = null;
88
+ for (const post of posts) {
89
+ const hit = post[field].find((term) => categorySlug(term) === slug);
90
+ if (hit !== undefined) {
91
+ if (label === null) label = hit;
92
+ matched.push(post);
93
+ }
94
+ }
95
+ return label === null ? null : { label, posts: matched };
96
+ }
97
+
98
+ /**
99
+ * "Ada Lovelace" -> "ada-lovelace". Author bylines are people's names, so two
100
+ * spellings of the same person must resolve to ONE archive slug — otherwise a
101
+ * migrated blog splits an author across two pages. Beyond the shared category
102
+ * slugify we first fold the two variant classes that show up in real bylines:
103
+ * - diacritics: "María García" == "Maria Garcia" (NFKD + strip combining marks)
104
+ * - apostrophes/quotes are DROPPED, not turned into a word break, so
105
+ * "Austin O'Brion" == "Austin Obrion" -> "austin-obrion" (WordPress-style),
106
+ * not "austin-o-brion" != "austin-obrion".
107
+ * categorySlug is intentionally left unfolded so existing category/tag URLs
108
+ * (and their redirects/sitemap entries) stay byte-stable.
109
+ */
110
+ export function authorSlug(name: string): string {
111
+ const folded = name
112
+ .normalize("NFKD") // decompose "é" -> "e" + combining acute
113
+ .replace(/[\u0300-\u036f]/g, "") // strip the combining marks
114
+ .replace(/['\u2018\u2019\u02bc\u0060]/g, ""); // drop apostrophes/quotes, do not split
115
+ return categorySlug(folded);
116
+ }
117
+
118
+ function normalizeStatus(raw: unknown): PostStatus {
119
+ return raw === "draft" || raw === "unlisted" || raw === "published" || raw === "scheduled"
120
+ ? raw
121
+ : "published";
122
+ }
123
+
124
+ // Legacy/seeded post JSON may omit `status`; default it to "published" on load so
125
+ // every downstream reader sees a concrete status.
126
+ function normalize(raw: BlogPost): BlogPost {
127
+ return { ...raw, status: normalizeStatus((raw as { status?: unknown }).status) };
128
+ }
129
+
130
+ function postsPrefix(tenantId: string, collection: string): string {
131
+ return `/tenants/${tenantId}/content/${collection}/posts/`;
132
+ }
133
+
134
+ function toSummary(p: BlogPost): BlogPostSummary {
135
+ return {
136
+ source: p.source,
137
+ slug: p.slug,
138
+ title: p.title,
139
+ seoTitle: p.seoTitle,
140
+ description: p.description,
141
+ excerpt: p.excerpt,
142
+ publishedAt: p.publishedAt,
143
+ modifiedAt: p.modifiedAt,
144
+ author: p.author,
145
+ categories: p.categories,
146
+ tags: p.tags,
147
+ status: p.status,
148
+ ...(p.scheduledAt ? { scheduledAt: p.scheduledAt } : {}),
149
+ ...(p.featuredImage ? { featuredImage: p.featuredImage } : {}),
150
+ ...(p.canonicalUrl ? { canonicalUrl: p.canonicalUrl } : {}),
151
+ ...(p.inLanguage ? { inLanguage: p.inLanguage } : {}),
152
+ ...(p.translationGroupId ? { translationGroupId: p.translationGroupId } : {}),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * The collection ids that actually have post content for a tenant, derived from
158
+ * the same glob the index is built from (no per-tenant config needed). Sorted for
159
+ * a stable admin ordering. A tenant with no blog content returns `[]`.
160
+ */
161
+ export async function listCollections(
162
+ tenantId: string,
163
+ content?: ArtifactSource,
164
+ ): Promise<string[]> {
165
+ const marker = `/tenants/${tenantId}/content/`;
166
+ const collections = new Set<string>();
167
+ const runtimePaths = canReadArtifacts(content)
168
+ ? await content.listArtifactPaths("")
169
+ : null;
170
+ if (runtimePaths !== null) {
171
+ for (const path of runtimePaths) {
172
+ const match = /^([^/]+)\/posts\/[^/]+\.json$/.exec(path);
173
+ if (match?.[1] && SEGMENT_RE.test(match[1])) collections.add(match[1]);
174
+ }
175
+ return [...collections].sort();
176
+ }
177
+ for (const key of Object.keys(postGlobs)) {
178
+ const at = key.indexOf(marker);
179
+ if (at < 0) continue;
180
+ const rest = key.slice(at + marker.length);
181
+ const collection = rest.slice(0, rest.indexOf("/"));
182
+ if (collection && SEGMENT_RE.test(collection)) collections.add(collection);
183
+ }
184
+ return [...collections].sort();
185
+ }
186
+
187
+ /**
188
+ * The derived index for `(tenant, collection)`, newest first. Live surfaces get
189
+ * published posts only; admin/preview pass `includeUnpublished` to also see
190
+ * drafts and unlisted posts.
191
+ */
192
+ export async function listPosts(
193
+ tenantId: string,
194
+ collection: string,
195
+ opts?: CollectionReadOptions,
196
+ ): Promise<BlogPostSummary[]> {
197
+ if (!SEGMENT_RE.test(collection)) return [];
198
+ const prefix = postsPrefix(tenantId, collection);
199
+ const runtimeContent = canReadArtifacts(opts?.content) ? opts.content : null;
200
+ const runtimePaths = runtimeContent
201
+ ? await runtimeContent.listArtifactPaths(`${collection}/posts/`)
202
+ : null;
203
+ const posts = runtimePaths !== null
204
+ ? (await Promise.all(
205
+ runtimePaths
206
+ .filter((path) => path.endsWith(".json"))
207
+ .map(async (path) => {
208
+ const raw = await runtimeContent?.readArtifact(path);
209
+ if (!raw) return null;
210
+ try {
211
+ return normalize(JSON.parse(raw) as BlogPost);
212
+ } catch {
213
+ return null;
214
+ }
215
+ }),
216
+ )).filter((post): post is BlogPost => post !== null)
217
+ : (await Promise.all(
218
+ Object.keys(postGlobs)
219
+ .filter((key) => key.includes(prefix))
220
+ .map((key) => postGlobs[key]!()),
221
+ )).map(normalize);
222
+ const includeUnpublished = opts?.includeUnpublished ?? false;
223
+ const now = opts?.now ?? new Date().toISOString();
224
+ return posts
225
+ .filter((p) => includeUnpublished || p.status === "published" || isDueScheduled(p, now))
226
+ .sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : a.publishedAt > b.publishedAt ? -1 : 0))
227
+ .map(toSummary);
228
+ }
229
+
230
+ /**
231
+ * Load a single post. On the live path a draft returns null (404) and an
232
+ * unlisted post still resolves; admin/preview pass `includeUnpublished` to load
233
+ * anything.
234
+ */
235
+ export async function getPost(
236
+ tenantId: string,
237
+ collection: string,
238
+ slug: string,
239
+ opts?: CollectionReadOptions,
240
+ ): Promise<BlogPost | null> {
241
+ if (!SEGMENT_RE.test(collection) || !SLUG_RE.test(slug)) return null;
242
+ if (canReadArtifacts(opts?.content)) {
243
+ const paths = await opts.content.listArtifactPaths(`${collection}/posts/`);
244
+ const raw = await opts.content.readArtifact(`${collection}/posts/${slug}.json`);
245
+ if (raw) {
246
+ try {
247
+ const post = normalize(JSON.parse(raw) as BlogPost);
248
+ if (!(opts.includeUnpublished ?? false) && !isVisibleOnLive(post, opts.now)) return null;
249
+ return post;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ if (paths !== null) return null;
255
+ }
256
+ const key = Object.keys(postGlobs).find(
257
+ (candidate) =>
258
+ candidate.includes(postsPrefix(tenantId, collection)) && candidate.endsWith(`/${slug}.json`),
259
+ );
260
+ if (!key) return null;
261
+ const post = normalize(await postGlobs[key]!());
262
+ if (!(opts?.includeUnpublished ?? false) && !isVisibleOnLive(post, opts?.now)) return null;
263
+ return post;
264
+ }
265
+
266
+ /**
267
+ * Every language variant sharing a `translationGroupId`, newest first. A filter
268
+ * over `listPosts`, so it inherits the visibility chokepoint verbatim: on the live
269
+ * path a draft/not-yet-due translation never appears, and an unlisted variant —
270
+ * which resolves at its own URL but is absent from every listing — is likewise
271
+ * absent here. Pass `includeUnpublished` for the admin/preview view of the group.
272
+ */
273
+ export async function listTranslations(
274
+ tenantId: string,
275
+ collection: string,
276
+ translationGroupId: string,
277
+ opts?: CollectionReadOptions,
278
+ ): Promise<BlogPostSummary[]> {
279
+ if (!translationGroupId) return [];
280
+ const posts = await listPosts(tenantId, collection, opts);
281
+ return posts.filter((p) => p.translationGroupId === translationGroupId);
282
+ }
283
+
284
+ /**
285
+ * Convenience for a route holding a slug: the full variant set for the article at
286
+ * `slug`, including the post itself. A singleton (no `translationGroupId`) returns
287
+ * exactly `[that post]` — the uniform "at least one" contract lets a caller render
288
+ * a switcher purely on `length > 1` without a separate group-id guard. Returns `[]`
289
+ * only when the post itself does not resolve (unknown, or hidden on the live path).
290
+ */
291
+ export async function getTranslations(
292
+ tenantId: string,
293
+ collection: string,
294
+ slug: string,
295
+ opts?: CollectionReadOptions,
296
+ ): Promise<BlogPostSummary[]> {
297
+ const post = await getPost(tenantId, collection, slug, opts);
298
+ if (!post) return [];
299
+ if (!post.translationGroupId) return [toSummary(post)];
300
+ return listTranslations(tenantId, collection, post.translationGroupId, opts);
301
+ }
302
+
303
+ /**
304
+ * Live-visible related posts for an article page: same primary category first
305
+ * (newest first, excluding the current post), then fall back to newest overall
306
+ * to fill up to `n`. Only published posts are candidates; the current post may
307
+ * itself be unlisted (its page still resolves) and is always excluded.
308
+ */
309
+ export async function relatedPosts(
310
+ tenantId: string,
311
+ collection: string,
312
+ slug: string,
313
+ n = 3,
314
+ content?: ArtifactSource,
315
+ ): Promise<BlogPostSummary[]> {
316
+ const pool = (await listPosts(tenantId, collection, { content })).filter((p) => p.slug !== slug);
317
+ const current = await getPost(tenantId, collection, slug, { content });
318
+ const category = current?.categories[0];
319
+ const result = category ? pool.filter((p) => p.categories.includes(category)) : [];
320
+ for (const p of pool) {
321
+ if (result.length >= n) break;
322
+ if (!result.some((r) => r.slug === p.slug)) result.push(p);
323
+ }
324
+ return result.slice(0, n);
325
+ }
326
+
327
+ export async function resolveRedirect(
328
+ tenantId: string,
329
+ collection: string,
330
+ slug: string,
331
+ content?: ArtifactSource,
332
+ ): Promise<string | null> {
333
+ if (!SEGMENT_RE.test(collection)) return null;
334
+ const redirects = await getRedirects(tenantId, collection, content);
335
+ return redirects[slug] ?? null;
336
+ }
337
+
338
+ /**
339
+ * The full `slug -> path` redirect map for `(tenant, collection)`, or `{}` when
340
+ * there is no `redirects.json`. The admin fast lane (B2.2) reads this to APPEND a
341
+ * redirect when a published post is deleted, without clobbering existing entries.
342
+ */
343
+ export async function getRedirects(
344
+ tenantId: string,
345
+ collection: string,
346
+ content?: ArtifactSource,
347
+ ): Promise<Record<string, string>> {
348
+ if (!SEGMENT_RE.test(collection)) return {};
349
+ if (canReadArtifacts(content)) {
350
+ const paths = await content.listArtifactPaths(`${collection}/`);
351
+ const raw = await content.readArtifact(`${collection}/redirects.json`);
352
+ if (raw) {
353
+ try {
354
+ return JSON.parse(raw) as Record<string, string>;
355
+ } catch {
356
+ return {};
357
+ }
358
+ }
359
+ if (paths !== null) return {};
360
+ }
361
+ const key = Object.keys(redirectGlobs).find(
362
+ (candidate) =>
363
+ candidate.includes(`/tenants/${tenantId}/content/${collection}/`) &&
364
+ candidate.endsWith("/redirects.json"),
365
+ );
366
+ if (!key) return {};
367
+ return (await redirectGlobs[key]!()) ?? {};
368
+ }
369
+
370
+ /**
371
+ * Optional per-tenant × per-collection author bios, keyed by author slug. Missing
372
+ * file returns {} so callers fall back to the byline name. Author pages resolve a
373
+ * bio with `getAuthors(t, c)[authorSlug(post.author)]`.
374
+ */
375
+ export async function getAuthors(
376
+ tenantId: string,
377
+ collection: string,
378
+ content?: ArtifactSource,
379
+ ): Promise<Record<string, AuthorBio>> {
380
+ if (!SEGMENT_RE.test(collection)) return {};
381
+ if (canReadArtifacts(content)) {
382
+ const paths = await content.listArtifactPaths(`${collection}/`);
383
+ const raw = await content.readArtifact(`${collection}/authors.json`);
384
+ if (raw) {
385
+ try {
386
+ return JSON.parse(raw) as Record<string, AuthorBio>;
387
+ } catch {
388
+ return {};
389
+ }
390
+ }
391
+ if (paths !== null) return {};
392
+ }
393
+ const key = Object.keys(authorsGlobs).find(
394
+ (candidate) =>
395
+ candidate.includes(`/tenants/${tenantId}/content/${collection}/`) &&
396
+ candidate.endsWith("/authors.json"),
397
+ );
398
+ if (!key) return {};
399
+ return (await authorsGlobs[key]!()) ?? {};
400
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Minimal, dependency-free Markdown -> sanitized HTML for the admin blog editor
3
+ * (see docs/plans/2026-08-21-blog-platform-workstream.md). The storefront
4
+ * ships no markdown library (see the "No new deps" convention in §1), and the
5
+ * migration package's `sanitize-html` is a node-only devtool — so authoring from
6
+ * the admin gets this small hand-rolled renderer instead.
7
+ *
8
+ * SAFETY: the output is fed to `set:html` in the reader route
9
+ * (`the-build/[slug].astro`), so it must never emit author-supplied HTML
10
+ * verbatim. All text is HTML-escaped BEFORE any tag is emitted, only the tag set
11
+ * this renderer produces can appear, and link hrefs / image `src`s are
12
+ * scheme-checked (`http(s)`/`mailto`/site-relative/anchor only — `javascript:`
13
+ * etc. fall back to plain text). Admin is already gated, but escaping is
14
+ * defense-in-depth.
15
+ *
16
+ * SCOPE (v1): headings, bold, italic, inline code, fenced code, links, images,
17
+ * blockquotes, unordered/ordered lists, horizontal rules, paragraphs. NOT a
18
+ * CommonMark implementation — no nested lists or tables.
19
+ */
20
+
21
+ // A control char that never appears in authored prose and is untouched by
22
+ // escapeHtml — a collision-free placeholder while code spans are set aside.
23
+ const CODE_MARK = String.fromCharCode(0);
24
+ const CODE_RE = new RegExp(`${CODE_MARK}(\\d+)${CODE_MARK}`, "g");
25
+
26
+ function escapeHtml(s: string): string {
27
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28
+ }
29
+
30
+ // Closes the one gap `escapeHtml` leaves open for attribute interpolation.
31
+ // The url substring reaching this point has ALREADY passed through the single
32
+ // escapeHtml(t) pass over the whole inline text (renderInline escapes before
33
+ // running the link regex), so `&`/`<`/`>` are already entity-encoded — only
34
+ // `"`/`'` remain untouched. Re-running full escapeHtml here would double-encode
35
+ // those already-escaped entities, so this only needs to close the quote gap.
36
+ function escapeAttrQuotes(s: string): string {
37
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
38
+ }
39
+
40
+ // Only permit hrefs that can't execute script. Text is already escaped by the
41
+ // time a URL reaches here, so a `javascript:`/`data:` payload arrives intact and
42
+ // is rejected -> the link renders as its (escaped) label text instead. Quote/
43
+ // angle-bracket characters are rejected outright (second line of defense on
44
+ // top of escapeAttrQuotes at the call site) so an attribute-breakout payload
45
+ // never matches the scheme allowlist in the first place.
46
+ function sanitizeUrl(url: string): string | null {
47
+ const u = url.trim();
48
+ if (/["'<>]/.test(u)) return null;
49
+ // `//host/path` is protocol-relative — a browser resolves it to an absolute
50
+ // cross-origin URL using the current scheme, so it must NOT be accepted
51
+ // under the "site-relative" `/` allowance (only a single, non-doubled slash
52
+ // is genuinely site-relative).
53
+ if (/^\/\//.test(u)) return null;
54
+ return /^(https?:\/\/|mailto:|\/|#)/i.test(u) ? u : null;
55
+ }
56
+
57
+ // Inline spans, applied to already-block-split text. Code spans are extracted
58
+ // first (and their contents escaped) so emphasis/link syntax inside `code` is
59
+ // left literal; everything else is escaped, then link/bold/italic tags are woven
60
+ // back in, then the code spans are restored.
61
+ function renderInline(text: string): string {
62
+ const codes: string[] = [];
63
+ let t = text.replace(/`([^`]+)`/g, (_m, code: string) => {
64
+ codes.push(`<code>${escapeHtml(code)}</code>`);
65
+ return `${CODE_MARK}${codes.length - 1}${CODE_MARK}`;
66
+ });
67
+ t = escapeHtml(t);
68
+ // Images before links: `![alt](url)` embeds a link-like `[alt](url)` span, so
69
+ // the link regex would otherwise claim it. Each emitted `<img>` is stashed in
70
+ // `codes` (same placeholder machinery as code spans) so the later link and
71
+ // emphasis passes can't reach into its `src`/`alt` attribute values — an
72
+ // `alt` of `a*b*c` must stay literal inside the attribute, not sprout an
73
+ // `<em>`. `src` is scheme-checked (data:/protocol-relative rejected) and both
74
+ // attribute values are quote-encoded on top of the whole-text escapeHtml pass.
75
+ t = t.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, alt: string, url: string) => {
76
+ const safe = sanitizeUrl(url);
77
+ if (!safe) return alt;
78
+ const img = `<img src="${escapeAttrQuotes(safe)}" alt="${escapeAttrQuotes(alt)}" loading="lazy" decoding="async">`;
79
+ codes.push(img);
80
+ return `${CODE_MARK}${codes.length - 1}${CODE_MARK}`;
81
+ });
82
+ t = t.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label: string, url: string) => {
83
+ const safe = sanitizeUrl(url);
84
+ return safe ? `<a href="${escapeAttrQuotes(safe)}">${label}</a>` : label;
85
+ });
86
+ t = t
87
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
88
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>");
89
+ t = t
90
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>")
91
+ .replace(/(^|[^a-zA-Z0-9])_([^_]+)_(?=[^a-zA-Z0-9]|$)/g, "$1<em>$2</em>");
92
+ return t.replace(CODE_RE, (_m, i: string) => codes[Number(i)]!);
93
+ }
94
+
95
+ const isBlank = (l: string): boolean => l.trim() === "";
96
+ const startsBlock = (l: string): boolean =>
97
+ /^```/.test(l) ||
98
+ /^#{1,6}\s+/.test(l) ||
99
+ /^>\s?/.test(l) ||
100
+ /^\s*[-*+]\s+/.test(l) ||
101
+ /^\s*\d+\.\s+/.test(l) ||
102
+ /^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(l);
103
+
104
+ export function renderMarkdown(md: string): string {
105
+ const lines = md.replace(/\r\n?/g, "\n").split("\n");
106
+ const out: string[] = [];
107
+ let i = 0;
108
+
109
+ while (i < lines.length) {
110
+ const line = lines[i]!;
111
+
112
+ if (isBlank(line)) {
113
+ i++;
114
+ continue;
115
+ }
116
+
117
+ const fence = line.match(/^```\s*([a-zA-Z0-9_-]*)\s*$/);
118
+ if (fence) {
119
+ const buf: string[] = [];
120
+ i++;
121
+ while (i < lines.length && !/^```\s*$/.test(lines[i]!)) buf.push(lines[i++]!);
122
+ i++; // consume closing fence (tolerate an unterminated block at EOF)
123
+ const cls = fence[1] ? ` class="language-${fence[1]}"` : "";
124
+ out.push(`<pre><code${cls}>${escapeHtml(buf.join("\n"))}</code></pre>`);
125
+ continue;
126
+ }
127
+
128
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
129
+ if (heading) {
130
+ const level = heading[1]!.length;
131
+ out.push(`<h${level}>${renderInline(heading[2]!.trim())}</h${level}>`);
132
+ i++;
133
+ continue;
134
+ }
135
+
136
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
137
+ out.push("<hr>");
138
+ i++;
139
+ continue;
140
+ }
141
+
142
+ if (/^>\s?/.test(line)) {
143
+ const buf: string[] = [];
144
+ while (i < lines.length && /^>\s?/.test(lines[i]!)) buf.push(lines[i++]!.replace(/^>\s?/, ""));
145
+ out.push(`<blockquote>${renderMarkdown(buf.join("\n"))}</blockquote>`);
146
+ continue;
147
+ }
148
+
149
+ if (/^\s*[-*+]\s+/.test(line)) {
150
+ const items: string[] = [];
151
+ while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i]!)) {
152
+ items.push(lines[i++]!.replace(/^\s*[-*+]\s+/, "").trim());
153
+ }
154
+ out.push(`<ul>${items.map((it) => `<li>${renderInline(it)}</li>`).join("")}</ul>`);
155
+ continue;
156
+ }
157
+
158
+ if (/^\s*\d+\.\s+/.test(line)) {
159
+ const items: string[] = [];
160
+ while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i]!)) {
161
+ items.push(lines[i++]!.replace(/^\s*\d+\.\s+/, "").trim());
162
+ }
163
+ out.push(`<ol>${items.map((it) => `<li>${renderInline(it)}</li>`).join("")}</ol>`);
164
+ continue;
165
+ }
166
+
167
+ const buf: string[] = [];
168
+ while (i < lines.length && !isBlank(lines[i]!) && !startsBlock(lines[i]!)) buf.push(lines[i++]!);
169
+ const text = buf.join(" ").trim();
170
+ if (text) out.push(`<p>${renderInline(text)}</p>`);
171
+ }
172
+
173
+ return out.join("\n");
174
+ }
@@ -1,4 +1,16 @@
1
+ // The live WordPress `/blog` deliberately keeps reading its committed, curated
2
+ // index.json (its order is hand-maintained and SEO-load-bearing). The generic
3
+ // derived engine lives in ./collection.ts and powers `the-build` + any new
4
+ // collection; switching `/blog` onto it is a later migration once order is
5
+ // encoded as data (see B0.1 §"/blog migration caution").
1
6
  import type { BlogPost, BlogPostSummary } from "./types.js";
7
+ import type { ContentProvider } from "../storyblok/provider.js";
8
+
9
+ type ArtifactSource = Pick<ContentProvider, "readArtifact" | "listArtifactPaths">;
10
+
11
+ function canReadArtifacts(value: ArtifactSource | undefined): value is ArtifactSource {
12
+ return typeof value?.listArtifactPaths === "function" && typeof value.readArtifact === "function";
13
+ }
2
14
 
3
15
  const indexGlobs = import.meta.glob<BlogPostSummary[]>(
4
16
  "../../../../../tenants/*/content/blog/index.json",
@@ -18,19 +30,66 @@ function tenantKey(keys: string[], tenantId: string, suffix: string): string | u
18
30
  return keys.find((key) => key.includes(segment) && key.endsWith(suffix));
19
31
  }
20
32
 
21
- export async function listBlogPosts(tenantId: string): Promise<BlogPostSummary[]> {
33
+ export async function listBlogPosts(
34
+ tenantId: string,
35
+ source?: ArtifactSource,
36
+ ): Promise<BlogPostSummary[]> {
37
+ if (canReadArtifacts(source)) {
38
+ const paths = await source.listArtifactPaths("blog/");
39
+ const raw = await source.readArtifact("blog/index.json");
40
+ if (raw) {
41
+ try {
42
+ return JSON.parse(raw) as BlogPostSummary[];
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+ if (paths !== null) return [];
48
+ }
22
49
  const key = tenantKey(Object.keys(indexGlobs), tenantId, "/index.json")
23
50
  ?? Object.keys(indexGlobs).find((candidate) => candidate.includes(`/tenants/${tenantId}/content/blog/index.json`));
24
51
  return key ? indexGlobs[key]!() : [];
25
52
  }
26
53
 
27
- export async function getBlogPost(tenantId: string, slug: string): Promise<BlogPost | null> {
54
+ export async function getBlogPost(
55
+ tenantId: string,
56
+ slug: string,
57
+ source?: ArtifactSource,
58
+ ): Promise<BlogPost | null> {
28
59
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) return null;
60
+ if (canReadArtifacts(source)) {
61
+ const paths = await source.listArtifactPaths("blog/posts/");
62
+ const raw = await source.readArtifact(`blog/posts/${slug}.json`);
63
+ if (raw) {
64
+ try {
65
+ return JSON.parse(raw) as BlogPost;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ if (paths !== null) return null;
71
+ }
29
72
  const key = tenantKey(Object.keys(postGlobs), tenantId, `/posts/${slug}.json`);
30
73
  return key ? postGlobs[key]!() : null;
31
74
  }
32
75
 
33
- export async function resolveBlogRedirect(tenantId: string, slug: string): Promise<string | null> {
76
+ export async function resolveBlogRedirect(
77
+ tenantId: string,
78
+ slug: string,
79
+ source?: ArtifactSource,
80
+ ): Promise<string | null> {
81
+ if (canReadArtifacts(source)) {
82
+ const paths = await source.listArtifactPaths("blog/");
83
+ const raw = await source.readArtifact("blog/redirects.json");
84
+ if (raw) {
85
+ try {
86
+ return (JSON.parse(raw) as Record<string, string>)[slug] ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ if (paths !== null) return null;
92
+ }
34
93
  const key = tenantKey(Object.keys(redirectGlobs), tenantId, "/redirects.json")
35
94
  ?? Object.keys(redirectGlobs).find((candidate) => candidate.includes(`/tenants/${tenantId}/content/blog/redirects.json`));
36
95
  if (!key) return null;