@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,146 @@
1
+ /**
2
+ * N2 — newsletter double-opt-in subscribe. `POST /api/newsletter/<collection>/subscribe`
3
+ * with body `{ email }` (JSON) or an `email` form field.
4
+ *
5
+ * Discipline (mirrors the other public POSTs, `blog/.../react.ts` / `newsletter.ts`):
6
+ * - `tenant_id` is resolved from `locals.tenant`, `collection` from the ROUTE —
7
+ * NEVER the request body (tenant is the isolation boundary).
8
+ * - A soft per-IP KV rate limit (`kvWindow`) stops trivial scripted signups.
9
+ * - `INSERT OR IGNORE` a `pending` row with a fresh opaque token + consent
10
+ * record (`consent_source` = referrer, `consent_ip_hash` = sha256(ip+salt) —
11
+ * NEVER a raw IP). A repeat subscribe is idempotent.
12
+ * - Confirmation email is STUBBED (no ESP in-repo yet — N4 wires it); a stub
13
+ * failure never fails the subscribe.
14
+ * - Never 500. A `fetch`/JSON caller gets a clean JSON body; a plain form POST
15
+ * is redirected back to the referrer so the page can show its own thanks.
16
+ */
17
+ import type { APIRoute } from "astro";
18
+ import { readD1, readKv, readEnv } from "@/lib/env";
19
+ import { fromD1 } from "@/lib/d1/catalog";
20
+ import { checkRateLimit } from "@/lib/rate/kvWindow";
21
+ import { NewsletterSubscribersStore } from "@/lib/newsletter/subscribers";
22
+ import { randomToken, hashConsentIp } from "@/lib/newsletter/consent";
23
+ import { sendConfirmationEmail } from "@/lib/newsletter/confirmationEmail";
24
+
25
+ export const prerender = false;
26
+
27
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
28
+ const COLLECTION_RE = /^[a-z0-9][a-z0-9._-]*$/;
29
+ const RATE_WINDOW_SECONDS = 3600;
30
+ const RATE_MAX_PER_WINDOW = 5;
31
+ /** Dev fallback only — production sets NEWSLETTER_CONSENT_SALT as a secret. */
32
+ const DEV_CONSENT_SALT = "newsletter-consent-dev-salt";
33
+
34
+ function json(body: unknown, status = 200): Response {
35
+ return new Response(JSON.stringify(body), {
36
+ status,
37
+ headers: { "content-type": "application/json; charset=utf-8" },
38
+ });
39
+ }
40
+
41
+ /** Best-effort client IP for rate-limit keying + consent record — CF header, else XFF. */
42
+ function clientIp(request: Request): string {
43
+ const cf = request.headers.get("cf-connecting-ip");
44
+ if (cf) return cf;
45
+ const xff = request.headers.get("x-forwarded-for");
46
+ if (xff) return xff.split(",")[0]!.trim();
47
+ return "unknown";
48
+ }
49
+
50
+ /** Read `email` from a JSON or form-encoded body; null if unparseable. */
51
+ async function readEmail(request: Request): Promise<string | null> {
52
+ try {
53
+ const type = request.headers.get("content-type") ?? "";
54
+ if (type.includes("application/json")) {
55
+ const body = (await request.json()) as { email?: unknown };
56
+ return typeof body.email === "string" ? body.email : null;
57
+ }
58
+ const value = (await request.formData()).get("email");
59
+ return typeof value === "string" ? value : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /** True when the caller wants JSON (fetch/JS) rather than a page navigation. */
66
+ function wantsJson(request: Request): boolean {
67
+ const ct = request.headers.get("content-type") ?? "";
68
+ if (ct.includes("application/json")) return true;
69
+ return (request.headers.get("accept") ?? "").includes("application/json");
70
+ }
71
+
72
+ /** Success response: JSON for fetch, else redirect back to the referrer with a flag. */
73
+ function ok(request: Request): Response {
74
+ if (wantsJson(request)) return json({ ok: true, status: "pending" });
75
+ const referer = request.headers.get("referer");
76
+ if (referer) {
77
+ try {
78
+ const back = new URL(referer);
79
+ back.searchParams.set("subscribed", "1");
80
+ return Response.redirect(back.toString(), 303);
81
+ } catch {
82
+ /* fall through */
83
+ }
84
+ }
85
+ return json({ ok: true, status: "pending" });
86
+ }
87
+
88
+ export const POST: APIRoute = async (context) => {
89
+ const tenantId = context.locals?.tenant?.tenant_id;
90
+ const collection = context.params.collection;
91
+ if (!tenantId || !collection || !COLLECTION_RE.test(collection)) {
92
+ return json({ ok: false, error: "not_found" }, 404);
93
+ }
94
+
95
+ try {
96
+ const email = (await readEmail(context.request))?.trim().toLowerCase() ?? "";
97
+ if (!EMAIL_RE.test(email)) {
98
+ return json({ ok: false, error: "invalid_email" }, 400);
99
+ }
100
+
101
+ const kv = await readKv("TENANT_CACHE");
102
+ const { allowed } = await checkRateLimit(
103
+ kv,
104
+ `newsletter-subscribe:${tenantId}:${clientIp(context.request)}`,
105
+ { windowSeconds: RATE_WINDOW_SECONDS, maxRequests: RATE_MAX_PER_WINDOW },
106
+ );
107
+ if (!allowed) return json({ ok: false, error: "rate_limited" }, 429);
108
+
109
+ const d1 = await readD1("STOREFRONT_APPS_DB");
110
+ if (!d1) {
111
+ // No apps DB binding (local/dev without D1): can't persist. Don't fail the
112
+ // visitor — log and acknowledge so the flow is exercisable in dev.
113
+ console.log(`[newsletter] subscribe (no D1) ${email} → ${tenantId}/${collection}`);
114
+ return ok(context.request);
115
+ }
116
+
117
+ const referer = context.request.headers.get("referer");
118
+ const consentSource = referer ? new URL(referer).pathname : null;
119
+ const salt = (await readEnv("NEWSLETTER_CONSENT_SALT")) ?? DEV_CONSENT_SALT;
120
+ const consentIpHash = await hashConsentIp(clientIp(context.request), salt);
121
+
122
+ const store = new NewsletterSubscribersStore(fromD1(d1));
123
+ const result = await store.subscribe({
124
+ tenantId,
125
+ collection,
126
+ email,
127
+ token: randomToken(),
128
+ consentSource,
129
+ consentIpHash,
130
+ });
131
+
132
+ // Only (re)send confirmation while still pending — never re-mail a confirmed
133
+ // or unsubscribed address. Best-effort; a stub failure must not fail signup.
134
+ if (result.status === "pending") {
135
+ const origin = new URL(context.request.url).origin;
136
+ const basePath = context.locals?.basePath ?? "";
137
+ const confirmUrl = `${origin}${basePath}/api/newsletter/confirm?token=${result.token}`;
138
+ await sendConfirmationEmail({ email, collection, confirmUrl }).catch(() => {});
139
+ }
140
+
141
+ return ok(context.request);
142
+ } catch {
143
+ // Never 500 on a signup — acknowledge and move on.
144
+ return ok(context.request);
145
+ }
146
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * N2 — newsletter double-opt-in confirm. `GET /api/newsletter/confirm?token=…`
3
+ * (the link in the confirmation email). Flips the addressed `pending` subscriber
4
+ * to `confirmed`; that timestamp + the stored consent source/ip-hash IS the GDPR
5
+ * consent record.
6
+ *
7
+ * The token is opaque + per-subscriber, so it identifies the exact row without
8
+ * an email in the URL; the lookup is still scoped to `locals.tenant`. Idempotent:
9
+ * re-clicking an already-confirmed link is a success, not an error. An unknown/
10
+ * stale token renders a clean "invalid link" page — never a 500.
11
+ */
12
+ import type { APIRoute } from "astro";
13
+ import { readD1 } from "@/lib/env";
14
+ import { fromD1 } from "@/lib/d1/catalog";
15
+ import { NewsletterSubscribersStore } from "@/lib/newsletter/subscribers";
16
+
17
+ export const prerender = false;
18
+
19
+ function page(title: string, message: string, status = 200): Response {
20
+ const esc = (s: string) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]!);
21
+ const html = `<!doctype html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8" />
25
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
26
+ <meta name="robots" content="noindex" />
27
+ <title>${esc(title)}</title>
28
+ <style>
29
+ :root { color-scheme: light dark; }
30
+ body { font-family: ui-serif, Georgia, serif; margin: 0; min-height: 100vh;
31
+ display: grid; place-items: center; padding: 2rem; text-align: center;
32
+ background: #f4f1ea; color: #2f3a34; }
33
+ .card { max-width: 30rem; }
34
+ h1 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 0.75rem; }
35
+ p { color: #5a655e; line-height: 1.6; }
36
+ a { color: #2f3a34; font-weight: 600; }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div class="card">
41
+ <h1>${esc(title)}</h1>
42
+ <p>${esc(message)}</p>
43
+ <p><a href="/">Back to the shop →</a></p>
44
+ </div>
45
+ </body>
46
+ </html>`;
47
+ return new Response(html, {
48
+ status,
49
+ headers: { "content-type": "text/html; charset=utf-8" },
50
+ });
51
+ }
52
+
53
+ export const GET: APIRoute = async (context) => {
54
+ const tenantId = context.locals?.tenant?.tenant_id;
55
+ const token = context.url.searchParams.get("token")?.trim();
56
+ if (!tenantId || !token) {
57
+ return page("Invalid link", "That confirmation link isn't valid. Try subscribing again.", 400);
58
+ }
59
+
60
+ try {
61
+ const d1 = await readD1("STOREFRONT_APPS_DB");
62
+ if (!d1) {
63
+ return page("Try again shortly", "We couldn't confirm your subscription just now. Please try again in a moment.", 503);
64
+ }
65
+ const store = new NewsletterSubscribersStore(fromD1(d1));
66
+ const result = await store.confirmByToken({ tenantId, token });
67
+ if (result === "not_found") {
68
+ return page("Invalid link", "That confirmation link is expired or already used. Try subscribing again.", 400);
69
+ }
70
+ // "confirmed" and "already" are both success — idempotent.
71
+ return page("You're confirmed.", "Thanks for confirming — you'll hear from us when something worth knowing lands.");
72
+ } catch {
73
+ return page("Try again shortly", "We couldn't confirm your subscription just now. Please try again in a moment.", 503);
74
+ }
75
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Newsletter one-click unsubscribe (newsletter-send epic, N3) — the endpoint the
3
+ * unsubscribe footer link and the RFC 8058 `List-Unsubscribe` header both point at.
4
+ * (N4 composes those headers/links; this unit is only the target they hit.)
5
+ *
6
+ * - GET /api/newsletter/unsubscribe?token=… — a clicked email-footer link. Flips
7
+ * the subscriber to `unsubscribed` and renders a plain confirmation page.
8
+ * - POST /api/newsletter/unsubscribe?token=… — the RFC 8058 `List-Unsubscribe-Post:
9
+ * List-Unsubscribe=One-Click` request a mailbox provider (Gmail/Yahoo) fires. Same
10
+ * effect, 200 with no interstitial and no confirmation step.
11
+ *
12
+ * The `token` (query string only — never the email in the URL) is an opaque per-
13
+ * subscriber capability, so this succeeds WITHOUT login in one click. It is idempotent
14
+ * (re-unsubscribing is a no-op success) and NON-LEAKING: an unknown token gets the exact
15
+ * same response as a real one, so an observer can't probe which tokens exist.
16
+ *
17
+ * NOTE (integration): Astro's global `security.checkOrigin` (default-on for a server
18
+ * build) 403s a cross-origin form-encoded POST before it reaches this handler, which
19
+ * would break the RFC 8058 one-click POST from a mailbox provider. See the unit report.
20
+ */
21
+ import type { APIContext } from "astro";
22
+ import { readD1 } from "@/lib/env";
23
+ import { fromD1 } from "@/lib/d1/catalog";
24
+ import { unsubscribeByToken } from "@/lib/newsletter/subscribers";
25
+
26
+ export const prerender = false;
27
+
28
+ /** Confirmation page for the clicked-link (GET) path. `noindex`; house serif style. */
29
+ function confirmationPage(): Response {
30
+ const html = `<!doctype html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="utf-8" />
34
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
35
+ <meta name="robots" content="noindex" />
36
+ <title>Unsubscribed</title>
37
+ <style>
38
+ :root { color-scheme: light dark; }
39
+ body { font-family: ui-serif, Georgia, serif; margin: 0; min-height: 100vh;
40
+ display: grid; place-items: center; padding: 2rem; text-align: center;
41
+ background: #f4f1ea; color: #2f3a34; }
42
+ .card { max-width: 30rem; }
43
+ h1 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 0.75rem; }
44
+ p { color: #5a655e; line-height: 1.6; }
45
+ a { color: #2f3a34; font-weight: 600; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <div class="card">
50
+ <h1>You're unsubscribed.</h1>
51
+ <p>You won't receive any more of these emails. Changed your mind? You can always sign up again.</p>
52
+ <p><a href="/">Back to the shop →</a></p>
53
+ </div>
54
+ </body>
55
+ </html>`;
56
+ return new Response(html, {
57
+ status: 200,
58
+ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
59
+ });
60
+ }
61
+
62
+ /** Apply the unsubscribe for the token in the query string. Best-effort: never throws,
63
+ * and returns the same value regardless of whether the token was real (non-leaking).
64
+ * When no D1 binding is present (local/dev) it's a silent no-op — the response shape
65
+ * is identical either way. */
66
+ async function applyUnsubscribe(context: APIContext): Promise<void> {
67
+ try {
68
+ const token = new URL(context.request.url).searchParams.get("token") ?? "";
69
+ const d1 = await readD1("STOREFRONT_APPS_DB");
70
+ if (!d1) return;
71
+ await unsubscribeByToken(fromD1(d1), token, new Date().toISOString());
72
+ } catch {
73
+ // Unsubscribe must never error out at the reader — degrade to the same success
74
+ // response. (A failed write is preferable to showing a subscriber an error.)
75
+ }
76
+ }
77
+
78
+ export async function GET(context: APIContext): Promise<Response> {
79
+ await applyUnsubscribe(context);
80
+ return confirmationPage();
81
+ }
82
+
83
+ export async function POST(context: APIContext): Promise<Response> {
84
+ await applyUnsubscribe(context);
85
+ // RFC 8058 one-click: a bare 200, no body of consequence, no confirmation step.
86
+ return new Response("Unsubscribed", {
87
+ status: 200,
88
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
89
+ });
90
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PUT/HEAD /api/publish/stage-bundle?key=versions/<digest>/<rel> — the
3
+ * authenticated Worker staging surface that writes bundle objects into the
4
+ * `STATIC_BUNDLES` R2 binding, so CI stages digests WITHOUT a static R2
5
+ * S3-compatible key pair. Auth (a CI `private_key_jwt` assertion, RFC 7523/8707,
6
+ * one-time `jti`) + the write logic live in `@/lib/publish/stageBundleUpload`;
7
+ * this route only resolves the runtime bindings/config.
8
+ *
9
+ * Fails CLOSED (503 "not configured") until a human wires the CI client's public
10
+ * JWKS (`STATIC_BUNDLE_UPLOAD_JWKS`) + the endpoint's own audience
11
+ * (`STATIC_BUNDLE_UPLOAD_AUDIENCE`) — the same "needs provisioning" gate the R2
12
+ * buckets themselves sit behind. It is EXEMPT from the demo login gate
13
+ * (loginGate.ts `isStaticBundleUploadPath`): it carries its own, stricter auth,
14
+ * so it must answer server-to-server rather than being masked by a "Coming soon"
15
+ * holding page.
16
+ */
17
+ import type { APIContext } from "astro";
18
+ import { readEnv, readKv, readR2 } from "@/lib/env";
19
+ import {
20
+ handleStageBundleUpload,
21
+ kvJtiReplayStore,
22
+ DEFAULT_STAGE_BUNDLE_UPLOAD_ISSUER,
23
+ type StageBundleUploadBucket,
24
+ type StageBundleUploadConfig,
25
+ type JtiKvLike,
26
+ } from "@/lib/publish/stageBundleUpload";
27
+
28
+ export const prerender = false;
29
+
30
+ async function handle(context: APIContext): Promise<Response> {
31
+ const bucket = (await readR2("STATIC_BUNDLES")) as StageBundleUploadBucket | undefined;
32
+ if (!bucket) {
33
+ return new Response(JSON.stringify({ error: "no_bundle_store" }), {
34
+ status: 503,
35
+ headers: { "content-type": "application/json" },
36
+ });
37
+ }
38
+
39
+ const jwksRaw = await readEnv("STATIC_BUNDLE_UPLOAD_JWKS");
40
+ const audience = await readEnv("STATIC_BUNDLE_UPLOAD_AUDIENCE");
41
+ if (!jwksRaw || !audience) {
42
+ return new Response(JSON.stringify({ error: "upload_not_configured" }), {
43
+ status: 503,
44
+ headers: { "content-type": "application/json" },
45
+ });
46
+ }
47
+ let jwks: StageBundleUploadConfig["jwks"];
48
+ try {
49
+ jwks = JSON.parse(jwksRaw) as StageBundleUploadConfig["jwks"];
50
+ } catch {
51
+ return new Response(JSON.stringify({ error: "upload_not_configured" }), {
52
+ status: 503,
53
+ headers: { "content-type": "application/json" },
54
+ });
55
+ }
56
+ const issuer =
57
+ (await readEnv("STATIC_BUNDLE_UPLOAD_ISSUER")) ?? DEFAULT_STAGE_BUNDLE_UPLOAD_ISSUER;
58
+
59
+ const kv = (await readKv("TENANT_CACHE")) as JtiKvLike | undefined;
60
+
61
+ return handleStageBundleUpload({
62
+ request: context.request,
63
+ bucket,
64
+ config: { jwks, issuer, audience },
65
+ ...(kv ? { jtiStore: kvJtiReplayStore(kv) } : {}),
66
+ });
67
+ }
68
+
69
+ export const PUT = handle;
70
+ export const HEAD = handle;
@@ -14,6 +14,8 @@
14
14
  */
15
15
  import type { APIRoute } from "astro";
16
16
  import { rateMetric, type WebVitalName, type WebVitalsSample } from "@/lib/analytics/webVitals";
17
+ import { readKv, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
18
+ import { recordBlogView } from "@/lib/blog/viewCounts";
17
19
 
18
20
  export const prerender = false;
19
21
 
@@ -38,14 +40,30 @@ function parseSample(raw: unknown): WebVitalsSample | null {
38
40
  };
39
41
  }
40
42
 
41
- export const POST: APIRoute = async ({ request }) => {
43
+ export const POST: APIRoute = async (context) => {
42
44
  try {
43
- const sample = parseSample(await request.json());
45
+ const sample = parseSample(await context.request.json());
44
46
  if (sample) {
45
47
  // eslint-disable-next-line no-console
46
48
  console.info("[rum:web-vitals]", JSON.stringify(sample));
47
49
  // TODO(F14 follow-up): forward to Analytics Engine and run
48
50
  // rumAlert.detectRegressions on the per-route/hour rollup.
51
+
52
+ // B3.3 — per-post blog view counts. CLS is emitted exactly once per page
53
+ // load (LCP/INP are conditional), so it is the reliable one-per-view
54
+ // signal; count off it only. `recordBlogView` no-ops unless `sample.path`
55
+ // resolves to a live post for the resolved tenant. Runs PAST the response
56
+ // so the beacon 204 is never delayed and the KV write never fails the
57
+ // request.
58
+ if (sample.metric === "cls") {
59
+ const tenantId = context.locals?.tenant?.tenant_id;
60
+ const kv = tenantId ? await readKv("TENANT_CACHE") : undefined;
61
+ if (tenantId && kv) {
62
+ const cfContext =
63
+ (context.locals as { cfContext?: CfExecutionContextLike })?.cfContext ?? null;
64
+ runAfterResponse(recordBlogView(kv, tenantId, sample.path), cfContext);
65
+ }
66
+ }
49
67
  }
50
68
  } catch {
51
69
  /* never fail a telemetry beacon */
@@ -1,37 +1,74 @@
1
1
  ---
2
2
  import Layout from "@/layouts/Layout.astro";
3
- import { getBlogPost, resolveBlogRedirect } from "@/lib/blog/provider.js";
4
- import { breadcrumbLd } from "@/lib/jsonld.js";
3
+ import BlogPaywall from "@/components/blog/BlogPaywall.astro";
4
+ import ReactionBar from "@/components/blog/ReactionBar.astro";
5
+ import { getBlogPost, listBlogPosts, resolveBlogRedirect } from "@/lib/blog/provider.js";
6
+ import { authorSlug, getTranslations } from "@/lib/blog/collection.js";
7
+ import { viewerHasBlogAccess } from "@/lib/blog/access.js";
8
+ import { articleBodyHtml } from "@/lib/blog/teaser.js";
9
+ import { readViewerSession, safeNext } from "@/lib/auth/route.js";
10
+ import { buildAlternates } from "@/lib/seo/alternates.js";
11
+ import { languageLabel } from "@/lib/i18n.js";
12
+ import { breadcrumbLd, blogPostingLd } from "@/lib/jsonld.js";
5
13
 
6
- const { tenant, canonicalBase, basePath } = Astro.locals;
14
+ const { tenant, canonicalBase, basePath, content } = Astro.locals;
7
15
  const slug = Astro.params.slug ?? "";
8
- const post = await getBlogPost(tenant.tenant_id, slug);
16
+ const post = await getBlogPost(tenant.tenant_id, slug, content);
9
17
  if (!post) {
10
- const redirect = await resolveBlogRedirect(tenant.tenant_id, slug);
18
+ const redirect = await resolveBlogRedirect(tenant.tenant_id, slug, content);
11
19
  if (redirect) return Astro.redirect(`${basePath}/blog/${redirect}/`, 301);
12
20
  return new Response(null, { status: 404 });
13
21
  }
14
22
 
23
+ // Entitlement (⟂ visibility): a gated post is still published/listed, but an
24
+ // unentitled reader sees a teaser + paywall rather than the full body. A teaser
25
+ // is a legitimate partial-content 200 — indexable, NOT noindexed (B4.0 spike §4).
26
+ const viewer = await readViewerSession(Astro);
27
+ const gated = (post.access ?? "public") !== "public";
28
+ const unlock = viewerHasBlogAccess(viewer, post);
29
+ const bodyHtml = articleBodyHtml(post.contentHtml, { gated, unlock });
30
+ const next = safeNext(`${basePath}/blog/${post.slug}/`);
31
+ const loginHref = `/auth/login?next=${encodeURIComponent(next)}`;
32
+
15
33
  const canonical = new URL(`/blog/${post.slug}/`, canonicalBase + "/").toString();
16
- const articleLd = {
17
- "@context": "https://schema.org",
18
- "@type": "BlogPosting",
19
- headline: post.title,
20
- description: post.description,
34
+ // The author archive (`/blog/author/<slug>`) lists posts from the curated blog
35
+ // index, and 404s for an author with no LISTED post (an unlisted-only or
36
+ // index-omitted byline). Resolve that up front so the byline links ONLY when the
37
+ // archive will actually render — a byline must never point at a missing page
38
+ // (the invariant `blog/author/[author].astro` documents). Same slug + same source
39
+ // the archive itself filters on, so the two can't disagree.
40
+ const authorArchiveSlug = authorSlug(post.author);
41
+ const authorHasArchive = (await listBlogPosts(tenant.tenant_id, content)).some(
42
+ (p) => authorSlug(p.author) === authorArchiveSlug,
43
+ );
44
+ const authorUrl = authorHasArchive
45
+ ? new URL(`/blog/author/${authorArchiveSlug}/`, canonicalBase + "/").toString()
46
+ : undefined;
47
+ const articleLd = blogPostingLd(post, {
21
48
  url: canonical,
22
- mainEntityOfPage: canonical,
23
- datePublished: post.publishedAt,
24
- dateModified: post.modifiedAt,
25
- author: { "@type": "Person", name: post.author },
26
- publisher: { "@type": "Organization", name: tenant.displayName, url: canonicalBase },
27
- ...(post.featuredImage ? { image: new URL(post.featuredImage.src, canonicalBase + "/").toString() } : {}),
28
- };
49
+ canonicalBase,
50
+ publisherName: tenant.displayName,
51
+ authorUrl,
52
+ });
29
53
  const crumbs = breadcrumbLd([
30
54
  { label: tenant.displayName, href: "/" },
31
55
  { label: "Blog", href: "/blog/" },
32
56
  { label: post.title, href: `/blog/${post.slug}/` },
33
57
  ], canonicalBase);
34
58
  const blogHref = `${basePath}/blog/`;
59
+ const authorHref = authorHasArchive
60
+ ? `${basePath}/blog/author/${authorArchiveSlug}/`
61
+ : null;
62
+
63
+ // I2 — hreflang alternates + per-variant <html lang>. Singleton ⇒ [] alternates
64
+ // and no `lang` override, so a non-translated post is byte-identical to today.
65
+ const translations = post.translationGroupId
66
+ ? await getTranslations(tenant.tenant_id, "blog", post.slug, { content })
67
+ : [];
68
+ const alternates = buildAlternates(translations, (s) =>
69
+ new URL(`/blog/${s}/`, canonicalBase + "/").toString(),
70
+ );
71
+ const lang = alternates.length ? (post.inLanguage ?? "en") : undefined;
35
72
  ---
36
73
 
37
74
  <Layout
@@ -41,6 +78,19 @@ const blogHref = `${basePath}/blog/`;
41
78
  ogType="article"
42
79
  ogImage={post.featuredImage?.src}
43
80
  jsonLd={[crumbs, articleLd]}
81
+ article={{
82
+ publishedTime: post.publishedAt,
83
+ modifiedTime: post.modifiedAt,
84
+ author: authorUrl,
85
+ section: post.categories[0],
86
+ tags: post.tags,
87
+ }}
88
+ imageAlt={post.featuredImage?.alt}
89
+ imageWidth={post.featuredImage?.width}
90
+ imageHeight={post.featuredImage?.height}
91
+ canonicalOverride={post.canonicalUrl}
92
+ alternates={alternates}
93
+ lang={lang}
44
94
  >
45
95
  <article class="article-shell">
46
96
  <nav class="breadcrumbs" aria-label="Breadcrumb"><a href={blogHref}>Blog</a><span aria-hidden="true">/</span><span>{post.categories[0] ?? "Article"}</span></nav>
@@ -49,10 +99,27 @@ const blogHref = `${basePath}/blog/`;
49
99
  <h1>{post.title}</h1>
50
100
  <p class="dek">{post.excerpt}</p>
51
101
  <div class="byline">
52
- <span>By {post.author}</span>
102
+ <span>By {authorHref
103
+ ? <a href={authorHref} class="byline-author" rel="author">{post.author}</a>
104
+ : <span class="byline-author">{post.author}</span>}</span>
53
105
  <span aria-hidden="true">·</span>
54
106
  <time datetime={post.publishedAt}>{new Date(post.publishedAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}</time>
55
107
  </div>
108
+ {translations.length > 1 && (
109
+ <nav class="lang-switcher" aria-label="Available languages">
110
+ {translations.map((variant) => {
111
+ const code = variant.inLanguage ?? "en";
112
+ const current = variant.slug === post.slug;
113
+ return (
114
+ <a
115
+ hreflang={code}
116
+ href={`${basePath}/blog/${variant.slug}/`}
117
+ aria-current={current ? "true" : undefined}
118
+ >{languageLabel(code)}</a>
119
+ );
120
+ })}
121
+ </nav>
122
+ )}
56
123
  </header>
57
124
  {post.featuredImage && (
58
125
  <img
@@ -70,8 +137,14 @@ const blogHref = `${basePath}/blog/`;
70
137
  <a href={blogHref}>← All articles</a>
71
138
  {post.tags.length > 0 && <div class="article-tags">{post.tags.map((tag) => <span>{tag}</span>)}</div>}
72
139
  </aside>
73
- <div class="blog-body" set:html={post.contentHtml}></div>
140
+ <div class="blog-main">
141
+ <div class="blog-body" set:html={bodyHtml}></div>
142
+ {gated && !unlock && (
143
+ <BlogPaywall access={post.access ?? "authenticated"} next={next} signedIn={!!viewer} loginHref={loginHref} />
144
+ )}
145
+ </div>
74
146
  </div>
147
+ <ReactionBar collection="blog" slug={post.slug} />
75
148
  </article>
76
149
  </Layout>
77
150
 
@@ -84,12 +157,19 @@ const blogHref = `${basePath}/blog/`;
84
157
  h1 { margin-top: .9rem; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 1.02; }
85
158
  .dek { max-width: 68ch; margin: 1.5rem auto 0; color: var(--color-muted); font-size: clamp(1rem, 2vw, 1.2rem); }
86
159
  .byline { display: flex; justify-content: center; gap: .55rem; margin-top: 1.35rem; color: var(--color-muted); font-size: .85rem; }
160
+ .byline-author { color: var(--color-accent-ink); font-weight: 700; text-decoration: none; }
161
+ .byline-author:hover { text-decoration: underline; text-underline-offset: .18em; }
162
+ .lang-switcher { display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem; margin-top: 1.1rem; }
163
+ .lang-switcher a { padding: .28rem .6rem; border: 1px solid var(--color-border); border-radius: var(--radius-full); color: var(--color-muted); font-size: .78rem; font-weight: 600; text-decoration: none; }
164
+ .lang-switcher a:hover { color: var(--color-accent-ink); border-color: var(--color-accent); }
165
+ .lang-switcher a[aria-current="true"] { color: var(--color-accent-ink); border-color: var(--color-accent); background: var(--color-surface); }
87
166
  .featured { display: block; width: 100%; max-height: 620px; object-fit: cover; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }
88
167
  .article-grid { display: grid; grid-template-columns: 190px minmax(0, 720px); justify-content: center; gap: clamp(2rem, 6vw, 5rem); margin-top: clamp(3rem, 7vw, 5.5rem); }
89
168
  aside { align-self: start; position: sticky; top: 7rem; }
90
169
  aside > a { color: var(--color-accent-ink); font-size: .88rem; font-weight: 700; }
91
170
  .article-tags { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: 1.2rem; }
92
171
  .article-tags span { padding: .3rem .55rem; border: 1px solid var(--color-border); border-radius: var(--radius-full); color: var(--color-muted); font-size: .7rem; }
172
+ .blog-main { min-width: 0; }
93
173
  .blog-body { min-width: 0; font-size: 1.05rem; line-height: 1.78; }
94
174
  .blog-body :global(p), .blog-body :global(ul), .blog-body :global(ol), .blog-body :global(blockquote), .blog-body :global(figure), .blog-body :global(table), .blog-body :global(pre) { margin-block: 1.25rem; }
95
175
  .blog-body :global(h2), .blog-body :global(h3), .blog-body :global(h4) { scroll-margin-top: 7rem; margin: 2.8rem 0 1rem; line-height: 1.18; }