@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/apps/storefront/astro.config.mjs +15 -0
  2. package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
  3. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
  4. package/apps/storefront/drizzle.config.apps.ts +13 -0
  5. package/apps/storefront/env.d.ts +10 -0
  6. package/apps/storefront/migrations/README.md +13 -7
  7. package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
  8. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  9. package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
  10. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  11. package/apps/storefront/migrations-apps/meta/_journal.json +20 -0
  12. package/apps/storefront/package.json +12 -1
  13. package/apps/storefront/perf/README.md +64 -0
  14. package/apps/storefront/perf/assert-budgets.ts +159 -0
  15. package/apps/storefront/playwright.config.ts +23 -0
  16. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  17. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  18. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  19. package/apps/storefront/src/components/ProductCard.astro +1 -0
  20. package/apps/storefront/src/components/admin/AdminPublishTab.astro +3562 -0
  21. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  22. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  23. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  24. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  25. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  26. package/apps/storefront/src/components/content/Callout.astro +75 -0
  27. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  28. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  29. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  30. package/apps/storefront/src/components/content/RichText.astro +44 -0
  31. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  32. package/apps/storefront/src/components/home/Hero.astro +14 -0
  33. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  34. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  35. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  36. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  37. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  38. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  39. package/apps/storefront/src/config/storeName.ts +34 -0
  40. package/apps/storefront/src/layouts/Layout.astro +97 -23
  41. package/apps/storefront/src/lib/activity/alerts.ts +428 -0
  42. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  43. package/apps/storefront/src/lib/activity/deployVersion.ts +127 -0
  44. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  45. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  46. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  47. package/apps/storefront/src/lib/activity/query.ts +403 -0
  48. package/apps/storefront/src/lib/activity/recordActivity.ts +105 -0
  49. package/apps/storefront/src/lib/activity/store.ts +122 -0
  50. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  51. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  52. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  53. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  54. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  55. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  56. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  57. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  58. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  59. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  60. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  61. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  62. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  63. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  64. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  65. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  66. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  67. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  68. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  69. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  70. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  71. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  72. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  73. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  74. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  75. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  76. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  77. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  78. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  79. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  80. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  81. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  82. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  83. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  84. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +219 -0
  85. package/apps/storefront/src/lib/auth/route.ts +60 -5
  86. package/apps/storefront/src/lib/auth/session.ts +8 -0
  87. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  88. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  89. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  90. package/apps/storefront/src/lib/blog/types.ts +26 -0
  91. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  92. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  93. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  94. package/apps/storefront/src/lib/content/callout.ts +78 -0
  95. package/apps/storefront/src/lib/content/index.ts +27 -0
  96. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  97. package/apps/storefront/src/lib/content/prose.ts +186 -0
  98. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  99. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  100. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  101. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  102. package/apps/storefront/src/lib/d1/schema-apps.ts +267 -0
  103. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  104. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  105. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  106. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  107. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  108. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  109. package/apps/storefront/src/lib/dev/vcBinding.ts +80 -0
  110. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  111. package/apps/storefront/src/lib/env.ts +63 -0
  112. package/apps/storefront/src/lib/jsonld.ts +12 -17
  113. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  114. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  115. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  116. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  117. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  118. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  119. package/apps/storefront/src/lib/publish/shipWorkspace.ts +369 -0
  120. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  121. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  122. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  123. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  124. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  125. package/apps/storefront/src/lib/tot/query.ts +32 -0
  126. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  127. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  128. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  129. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  130. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  131. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  132. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  133. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  134. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  135. package/apps/storefront/src/middleware/index.ts +45 -13
  136. package/apps/storefront/src/pages/404.astro +21 -9
  137. package/apps/storefront/src/pages/[...slug].astro +36 -2
  138. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  139. package/apps/storefront/src/pages/admin.astro +183 -9
  140. package/apps/storefront/src/pages/api/activity.ts +137 -0
  141. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  142. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +86 -0
  143. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  144. package/apps/storefront/src/pages/api/apps/admin/install.ts +125 -0
  145. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  146. package/apps/storefront/src/pages/api/apps/admin/resume.ts +79 -0
  147. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +80 -0
  148. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +98 -0
  149. package/apps/storefront/src/pages/api/apps/admin/update.ts +126 -0
  150. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +70 -0
  151. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  152. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +183 -0
  153. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  154. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +162 -0
  155. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  156. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  157. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  158. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  159. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  160. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  161. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  162. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +74 -0
  163. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  164. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  165. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  166. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  167. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  168. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  169. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  170. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  171. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  172. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  173. package/apps/storefront/src/pages/auth/login.astro +202 -45
  174. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  175. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  176. package/apps/storefront/src/pages/blog/index.astro +98 -0
  177. package/apps/storefront/src/pages/capabilities.astro +8 -0
  178. package/apps/storefront/src/pages/cockpit.astro +470 -69
  179. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  180. package/apps/storefront/src/pages/collections/index.astro +10 -2
  181. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  182. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  183. package/apps/storefront/src/pages/index.astro +57 -0
  184. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  185. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  186. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  187. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  188. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  189. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  190. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  191. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  192. package/apps/storefront/src/styles/fonts.css +54 -0
  193. package/apps/storefront/src/styles/global.css +22 -2
  194. package/apps/storefront/src/themes/schema.ts +3 -25
  195. package/apps/storefront/tsconfig.json +1 -1
  196. package/apps/storefront/vitest.config.ts +4 -1
  197. package/package.json +1 -1
  198. package/packages/public-runtime/src/activity/README.md +146 -0
  199. package/packages/public-runtime/src/activity/catalog.ts +501 -0
  200. package/packages/public-runtime/src/activity/event.ts +168 -0
  201. package/packages/public-runtime/src/activity/index.ts +16 -0
  202. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  203. package/packages/public-runtime/src/candidate-index.ts +324 -0
  204. package/packages/public-runtime/src/checkout.ts +94 -2
  205. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  206. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  207. package/packages/public-runtime/src/compliance/index.ts +22 -0
  208. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  209. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  210. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  211. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  212. package/packages/public-runtime/src/csp.ts +27 -3
  213. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  214. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  215. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  216. package/packages/public-runtime/src/extension-contract.ts +2 -1
  217. package/packages/public-runtime/src/hash.ts +25 -0
  218. package/packages/public-runtime/src/index.ts +12 -0
  219. package/packages/public-runtime/src/membership.ts +353 -0
  220. package/packages/public-runtime/src/product.ts +24 -2
  221. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  222. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  223. package/packages/public-runtime/src/tenant.ts +236 -0
  224. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  225. package/scripts/dev/publish.mjs +158 -0
  226. package/scripts/dev/transient-files.mjs +2 -1
  227. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  228. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  229. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  230. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  231. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  232. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  233. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  234. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  235. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  236. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  237. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  238. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  239. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  240. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  241. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  242. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  243. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  244. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  245. package/tenants/home/public/pages/storefront.css +23 -0
  246. package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1784820107464,
9
+ "tag": "0000_fast_millenium_guard",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1787074116807,
16
+ "tag": "0001_woozy_lyja",
17
+ "breakpoints": true
18
+ }
19
+ ]
20
+ }
@@ -11,7 +11,7 @@
11
11
  "start": "astro dev",
12
12
  "prebuild": "node ../../scripts/build/copy-tenant-assets.mjs",
13
13
  "build": "astro build",
14
- "preview": "wrangler dev ./dist/_worker.js/index.js",
14
+ "preview": "wrangler dev",
15
15
  "typecheck": "astro check && tsc --noEmit",
16
16
  "astro": "astro",
17
17
  "test": "NODE_OPTIONS=--experimental-sqlite vitest run"
@@ -19,6 +19,13 @@
19
19
  "dependencies": {
20
20
  "@astrojs/cloudflare": "^14.0.1",
21
21
  "@astrojs/preact": "^6.0.0",
22
+ "@fontsource/inter": "5.3.0",
23
+ "@fontsource/jetbrains-mono": "5.3.0",
24
+ "@fontsource/montserrat": "5.3.0",
25
+ "@fontsource/plus-jakarta-sans": "5.3.0",
26
+ "@fontsource/source-serif-4": "5.3.0",
27
+ "@fontsource/space-grotesk": "5.3.0",
28
+ "@fontsource/space-mono": "5.3.0",
22
29
  "@storyblok/astro": "^10.0.0",
23
30
  "@tailwindcss/vite": "^4.3.1",
24
31
  "@tot/public-runtime": "*",
@@ -31,9 +38,13 @@
31
38
  },
32
39
  "devDependencies": {
33
40
  "@astrojs/check": "^0.9.4",
41
+ "@axe-core/playwright": "^4.12.1",
42
+ "@playwright/test": "^1.61.1",
43
+ "axe-core": "^4.12.1",
34
44
  "dependency-cruiser": "^18.0.0",
35
45
  "drizzle-kit": "^0.31.10",
36
46
  "drizzle-orm": "^0.45.2",
47
+ "tsx": "^4.22.4",
37
48
  "typescript": "^5.6.3",
38
49
  "vitest": "2.1.8",
39
50
  "wrangler": "^4.105.0"
@@ -0,0 +1,64 @@
1
+ # Performance budgets & RUM (F14)
2
+
3
+ The storefront's go-live framework enforces a performance gate. This directory
4
+ is the **lab half** (a checked-in budget manifest + a CI-runnable assertion
5
+ harness); the **field half** (Real User Monitoring) lives in
6
+ `../src/lib/analytics/`.
7
+
8
+ ## The pieces
9
+
10
+ | File | Role |
11
+ |------|------|
12
+ | `../src/lib/analytics/budgets.json` | **Source of truth.** Route budgets (LCP/INP/CLS p75 + JS/CSS/image/total KiB) + the third-party script allowlist. Edit here; everything derives from it. |
13
+ | `../src/lib/analytics/perfBudgets.ts` | Typed lens + **pure** budget-assertion logic (`evaluateManifest`) + the Lighthouse-budgets exporter (`toLighthouseBudgets`). Unit-tested in `perfBudgets.test.ts`. |
14
+ | `../src/lib/analytics/lighthouseReport.ts` | Pure adapter: a Lighthouse result (LHR JSON) → a `RouteMeasurement` the gate evaluates. Unit-tested in `lighthouseReport.test.ts`. |
15
+ | `assert-budgets.ts` | Thin CLI: reads measurements / LHRs, runs the gate, exits non-zero on a violation. |
16
+ | `../src/lib/analytics/webVitals.ts` | Field RUM beacon (LCP/INP/CLS from real visitors → the RUM sink). |
17
+ | `../src/lib/analytics/rumAlert.ts` | Pure sink-side regression-alert contract; feeds F16's `core-web-vitals` monitor. Unit-tested in `rumAlert.test.ts`. |
18
+
19
+ The budget numbers are enforced identically in three places, all reading the one
20
+ manifest: the lab gate here, a Lighthouse `--budget-path` run (via
21
+ `toLighthouseBudgets`), and the field RUM alert (`rumAlert.detectRegressions`).
22
+
23
+ ## Running the lab gate
24
+
25
+ ```bash
26
+ # 1. emit a Lighthouse-native budgets file (feed to `lighthouse --budget-path`)
27
+ pnpm --filter @tot/storefront perf:budgets:emit # → perf/lighthouse-budgets.json
28
+
29
+ # 2a. assert Lighthouse result JSON, one --lhr per route
30
+ pnpm --filter @tot/storefront perf:budgets -- \
31
+ --lhr home=home.lhr.json --lhr pdp=pdp.lhr.json --require-all
32
+
33
+ # 2b. …or assert a tool-neutral measurement report from any collector
34
+ pnpm --filter @tot/storefront perf:budgets -- --measurements report.json
35
+ ```
36
+
37
+ Exit `0` = within budget, `1` = a budget was violated (or, with `--require-all`,
38
+ a budgeted route was never measured), `2` = bad usage.
39
+
40
+ ## Preview-validation seam (live lab run — follow-up)
41
+
42
+ The pure logic (gate + LHR extraction) is **green in CI today** via the unit
43
+ tests. Wiring an actual Lighthouse run against a live preview URL is the
44
+ remaining step:
45
+
46
+ 1. `perf:budgets:emit` → `lighthouse-budgets.json`.
47
+ 2. For each route's `samplePath`, run Lighthouse against the preview URL with
48
+ `--budget-path=perf/lighthouse-budgets.json --output=json`, saving each LHR.
49
+ 3. `perf:budgets --lhr <route>=<lhr.json> … --require-all` — its exit code gates
50
+ the preview promotion.
51
+
52
+ Add step 2's Lighthouse invocation to the preview-validation workflow (or LHCI)
53
+ to make the gate enforce against real page loads. Until then the manifest,
54
+ exporter, extractor, and gate are all covered by unit tests so the numbers can't
55
+ silently drift.
56
+
57
+ ## Field RUM
58
+
59
+ `startWebVitals()` (from `../src/lib/analytics/webVitals.ts`) is called from the
60
+ client entry; it measures LCP/INP/CLS per real navigation, buckets each into the
61
+ same budgeted route id the lab gate uses, and beacons finished samples to the RUM
62
+ sink (`RUM_BEACON_PATH`). The sink rolls samples up per route/hour and calls
63
+ `rumAlert.detectRegressions()`; any returned `RegressionAlert` trips F16's
64
+ `core-web-vitals` monitor.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * F14 — Lab budget-assertion harness (the enforceable lab check).
3
+ *
4
+ * This is the thin CLI seam that preview validation calls to FAIL a build whose
5
+ * pages blow the performance budgets. All the decision logic lives in the pure,
6
+ * unit-tested modules under `src/lib/analytics/` — this file only does I/O:
7
+ * read a measurement report (or Lighthouse results), run the gate, print, exit.
8
+ *
9
+ * exit 0 → every measured route is within budget
10
+ * exit 1 → at least one budget violated (or, with --require-all, a route was
11
+ * never measured)
12
+ * exit 2 → bad usage / unreadable input
13
+ *
14
+ * Modes
15
+ * -----
16
+ * Emit the Lighthouse budgets file (feed it to `lighthouse --budget-path` /
17
+ * LHCI so Lighthouse enforces the same numbers directly):
18
+ * tsx perf/assert-budgets.ts --emit-lighthouse-budgets perf/lighthouse-budgets.json
19
+ *
20
+ * Assert a tool-neutral measurement report (array of RouteMeasurement, or
21
+ * `{ "measurements": [...] }`), from any collector:
22
+ * tsx perf/assert-budgets.ts --measurements report.json [--require-all]
23
+ *
24
+ * Assert Lighthouse result JSON files, one per route (`--lhr <routeId>=<lhr.json>`,
25
+ * repeatable):
26
+ * tsx perf/assert-budgets.ts --lhr home=home.lhr.json --lhr pdp=pdp.lhr.json
27
+ *
28
+ * How preview validation wires this (documented seam — the live run is a
29
+ * follow-up; the LOGIC is green in CI via the unit tests):
30
+ * 1. `pnpm --filter @tot/storefront perf:budgets:emit` → lighthouse-budgets.json
31
+ * 2. run Lighthouse against the preview URL per route with `--budget-path` +
32
+ * `--output=json`, saving each LHR
33
+ * 3. `tsx perf/assert-budgets.ts --lhr home=... --lhr pdp=... --require-all`
34
+ * Step 3's exit code gates the preview promotion.
35
+ */
36
+ import { readFileSync, writeFileSync } from "node:fs";
37
+ import {
38
+ evaluateManifest,
39
+ formatBudgetReport,
40
+ toLighthouseBudgets,
41
+ ROUTE_IDS,
42
+ type RouteId,
43
+ type RouteMeasurement,
44
+ } from "../src/lib/analytics/perfBudgets.js";
45
+ import { lhrToMeasurement, type LighthouseResult } from "../src/lib/analytics/lighthouseReport.js";
46
+
47
+ function die(msg: string, code = 2): never {
48
+ process.stderr.write(`assert-budgets: ${msg}\n`);
49
+ process.exit(code);
50
+ }
51
+
52
+ function readJson(path: string): unknown {
53
+ try {
54
+ return JSON.parse(readFileSync(path, "utf8"));
55
+ } catch (err) {
56
+ return die(`cannot read/parse ${path}: ${(err as Error).message}`);
57
+ }
58
+ }
59
+
60
+ function isRouteId(v: string): v is RouteId {
61
+ return (ROUTE_IDS as readonly string[]).includes(v);
62
+ }
63
+
64
+ function parseArgs(argv: readonly string[]) {
65
+ const opts = {
66
+ emit: undefined as string | undefined,
67
+ measurements: undefined as string | undefined,
68
+ lhr: [] as Array<{ route: RouteId; path: string }>,
69
+ requireAll: false,
70
+ };
71
+ for (let i = 0; i < argv.length; i++) {
72
+ const a = argv[i];
73
+ switch (a) {
74
+ case "--emit-lighthouse-budgets":
75
+ opts.emit = argv[++i] ?? die("--emit-lighthouse-budgets needs a path");
76
+ break;
77
+ case "--measurements":
78
+ opts.measurements = argv[++i] ?? die("--measurements needs a path");
79
+ break;
80
+ case "--lhr": {
81
+ const spec = argv[++i] ?? die("--lhr needs <routeId>=<path>");
82
+ const eq = spec.indexOf("=");
83
+ if (eq < 0) die(`--lhr expects <routeId>=<path>, got "${spec}"`);
84
+ const route = spec.slice(0, eq);
85
+ if (!isRouteId(route)) die(`--lhr: "${route}" is not a known route id`);
86
+ opts.lhr.push({ route, path: spec.slice(eq + 1) });
87
+ break;
88
+ }
89
+ case "--require-all":
90
+ opts.requireAll = true;
91
+ break;
92
+ case "-h":
93
+ case "--help":
94
+ process.stdout.write(
95
+ "usage: assert-budgets.ts [--emit-lighthouse-budgets <path>] " +
96
+ "[--measurements <path>] [--lhr <routeId>=<path> ...] [--require-all]\n",
97
+ );
98
+ process.exit(0);
99
+ default:
100
+ die(`unknown argument "${a}"`);
101
+ }
102
+ }
103
+ return opts;
104
+ }
105
+
106
+ function collectMeasurements(opts: ReturnType<typeof parseArgs>): RouteMeasurement[] {
107
+ const measurements: RouteMeasurement[] = [];
108
+
109
+ if (opts.measurements) {
110
+ const raw = readJson(opts.measurements);
111
+ const arr = Array.isArray(raw)
112
+ ? raw
113
+ : raw && typeof raw === "object" && Array.isArray((raw as { measurements?: unknown }).measurements)
114
+ ? (raw as { measurements: unknown[] }).measurements
115
+ : die(`${opts.measurements}: expected an array or { "measurements": [...] }`);
116
+ for (const m of arr as RouteMeasurement[]) {
117
+ if (!m || typeof m !== "object" || !isRouteId((m as RouteMeasurement).routeId)) {
118
+ die(`${opts.measurements}: every entry needs a valid "routeId"`);
119
+ }
120
+ measurements.push(m as RouteMeasurement);
121
+ }
122
+ }
123
+
124
+ for (const { route, path } of opts.lhr) {
125
+ measurements.push(lhrToMeasurement(readJson(path) as LighthouseResult, route));
126
+ }
127
+
128
+ return measurements;
129
+ }
130
+
131
+ function main() {
132
+ const opts = parseArgs(process.argv.slice(2));
133
+
134
+ if (opts.emit) {
135
+ writeFileSync(opts.emit, JSON.stringify(toLighthouseBudgets(), null, 2) + "\n");
136
+ process.stdout.write(`assert-budgets: wrote Lighthouse budgets → ${opts.emit}\n`);
137
+ return;
138
+ }
139
+
140
+ const measurements = collectMeasurements(opts);
141
+ if (measurements.length === 0) {
142
+ die("no measurements: pass --measurements <path> and/or --lhr <routeId>=<path>");
143
+ }
144
+
145
+ const report = evaluateManifest(measurements);
146
+ process.stdout.write(formatBudgetReport(report) + "\n");
147
+
148
+ const missingFatal = opts.requireAll && report.missingRoutes.length > 0;
149
+ if (!report.passed || missingFatal) {
150
+ if (missingFatal) {
151
+ process.stderr.write(
152
+ `assert-budgets: --require-all set but no measurement for: ${report.missingRoutes.join(", ")}\n`,
153
+ );
154
+ }
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ main();
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from "@playwright/test";
2
+ import { PARENT_PORT } from "./e2e/fixtures/ports.ts";
3
+
4
+ /**
5
+ * PrivateApps epic, D5 Chunk F. The storefront has no prior Playwright setup
6
+ * (`packages/migration` depends on `playwright-core` for its own screenshot
7
+ * tooling, not a `@playwright/test` runner) — this is the first. Scoped to
8
+ * `e2e/` and, for now, the one widget-sandbox spec: see `e2e/fixtures/server.ts`
9
+ * for the "why a fixture server, not the full Cloudflare Worker" scope decision.
10
+ */
11
+ export default defineConfig({
12
+ testDir: "./e2e",
13
+ fullyParallel: false,
14
+ reporter: "list",
15
+ use: {
16
+ baseURL: `http://127.0.0.1:${PARENT_PORT}`,
17
+ },
18
+ webServer: {
19
+ command: "npx tsx e2e/fixtures/server.ts",
20
+ url: `http://127.0.0.1:${PARENT_PORT}`,
21
+ timeout: 15_000,
22
+ },
23
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Client behavior for /dashboard/<appDomain>/apps (PrivateApps epic, D8 Chunk C).
3
+ *
4
+ * Served as a STATIC same-origin file (public/) and referenced via
5
+ * <script src="/js/dashboard-apps.js">, so the strict production CSP allows it via
6
+ * `script-src 'self'` — no nonce, no per-build hash. Mirrors dashboard-team.js exactly.
7
+ *
8
+ * The tenant appDomain and the manage-capability are read from the #apps
9
+ * container's `data-tenant`/`data-can-manage` attributes (server-resolved from
10
+ * the SAME `MANAGE_ROLES` set the admin routes gate on) — no server-injected
11
+ * values beyond those two, so this file is fully static.
12
+ */
13
+ (function () {
14
+ const $ = (id) => document.getElementById(id);
15
+ const container = $("apps");
16
+ if (!container) return;
17
+ const tenant = container.dataset.tenant;
18
+ const canManage = container.dataset.canManage === "true";
19
+
20
+ function fmtDuration(ms) {
21
+ if (ms == null) return "—";
22
+ return ms >= 1000 ? (ms / 1000).toFixed(1) + "s" : ms + "ms";
23
+ }
24
+
25
+ function badge(text, cls) {
26
+ const span = document.createElement("span");
27
+ span.className = "badge " + cls;
28
+ span.textContent = text;
29
+ return span;
30
+ }
31
+
32
+ function renderApp(app) {
33
+ const card = document.createElement("div");
34
+ card.className = "card";
35
+
36
+ const top = document.createElement("div");
37
+ top.className = "top";
38
+ const name = document.createElement("span");
39
+ name.className = "name";
40
+ name.textContent = app.appId;
41
+ top.appendChild(name);
42
+ const badges = document.createElement("div");
43
+ badges.appendChild(badge(app.status, "status-" + app.status));
44
+ badges.appendChild(badge(app.health.status, "health-" + app.health.status));
45
+ badges.appendChild(badge(app.egressStatus, "egress-" + app.egressStatus));
46
+ top.appendChild(badges);
47
+ card.appendChild(top);
48
+
49
+ const dl = document.createElement("dl");
50
+ dl.className = "meta";
51
+ function row(term, value) {
52
+ const dt = document.createElement("dt");
53
+ dt.textContent = term;
54
+ const dd = document.createElement("dd");
55
+ dd.textContent = value;
56
+ dl.appendChild(dt);
57
+ dl.appendChild(dd);
58
+ }
59
+ row("Env", app.env);
60
+ row("Scopes", (app.scopes || []).join(", ") || "none");
61
+ row("Webhook", app.hasWebhookEndpoint ? "configured" : "not configured");
62
+ row("p95 delivery", fmtDuration(app.health.p95DeliveryMs));
63
+ row("Failure streak", String(app.health.failureStreak));
64
+ row("Dead-lettered", String(app.health.dlqCount));
65
+ if (app.health.lastSuccessAt) row("Last success", app.health.lastSuccessAt);
66
+ card.appendChild(dl);
67
+
68
+ if (app.health.recentTraceIds && app.health.recentTraceIds.length) {
69
+ const traces = document.createElement("p");
70
+ traces.className = "traces";
71
+ traces.textContent = "Recent traces: " + app.health.recentTraceIds.join(", ");
72
+ card.appendChild(traces);
73
+ }
74
+
75
+ if (canManage) {
76
+ const actions = document.createElement("div");
77
+ actions.className = "actions";
78
+
79
+ if (app.status === "active") {
80
+ actions.appendChild(actionBtn("Suspend", "suspend", app.id, card, false));
81
+ } else if (app.status === "suspended") {
82
+ actions.appendChild(actionBtn("Resume", "resume", app.id, card, false));
83
+ }
84
+ if (app.status !== "uninstalled") {
85
+ actions.appendChild(actionBtn("Rotate credentials", "credentials/rotate", app.id, card, false));
86
+ actions.appendChild(actionBtn("Uninstall", "uninstall", app.id, card, true));
87
+ }
88
+ card.appendChild(actions);
89
+
90
+ const msg = document.createElement("div");
91
+ msg.className = "msg";
92
+ card.appendChild(msg);
93
+ }
94
+
95
+ return card;
96
+ }
97
+
98
+ function actionBtn(label, action, installId, card, danger) {
99
+ const btn = document.createElement("button");
100
+ btn.type = "button";
101
+ btn.className = "rowbtn" + (danger ? " danger" : "");
102
+ btn.textContent = label;
103
+ btn.addEventListener("click", () => runAction(action, installId, card, btn));
104
+ return btn;
105
+ }
106
+
107
+ async function runAction(action, installId, card, btn) {
108
+ const msg = card.querySelector(".msg");
109
+ btn.disabled = true;
110
+ if (msg) { msg.className = "msg"; msg.textContent = ""; }
111
+ try {
112
+ const res = await fetch("/api/apps/admin/" + action + "?tenant=" + encodeURIComponent(tenant), {
113
+ method: "POST",
114
+ headers: { "content-type": "application/json", accept: "application/json" },
115
+ body: JSON.stringify({ installId }),
116
+ });
117
+ const body = await res.json().catch(() => ({}));
118
+ if (res.ok) {
119
+ loadApps();
120
+ } else if (msg) {
121
+ msg.className = "msg err";
122
+ msg.textContent = body.error || ("Couldn't " + action + " (HTTP " + res.status + ").");
123
+ btn.disabled = false;
124
+ }
125
+ } catch (e) {
126
+ if (msg) {
127
+ msg.className = "msg err";
128
+ msg.textContent = "Couldn't " + action + ": " + (e && e.message ? e.message : e);
129
+ }
130
+ btn.disabled = false;
131
+ }
132
+ }
133
+
134
+ function renderApps(apps) {
135
+ container.replaceChildren();
136
+ if (!apps || !apps.length) {
137
+ const p = document.createElement("p");
138
+ p.className = "muted";
139
+ p.textContent = "No apps installed yet.";
140
+ container.appendChild(p);
141
+ return;
142
+ }
143
+ for (const app of apps) container.appendChild(renderApp(app));
144
+ }
145
+
146
+ function renderError(text) {
147
+ container.replaceChildren();
148
+ const p = document.createElement("p");
149
+ p.className = "muted";
150
+ p.textContent = text;
151
+ container.appendChild(p);
152
+ }
153
+
154
+ async function loadApps() {
155
+ try {
156
+ const res = await fetch("/api/apps/admin/health?tenant=" + encodeURIComponent(tenant), {
157
+ headers: { accept: "application/json" },
158
+ });
159
+ const body = await res.json().catch(() => ({}));
160
+ if (res.ok) {
161
+ renderApps(body.apps);
162
+ } else if (res.status === 403) {
163
+ renderError("You don't have access to this vendor's apps.");
164
+ } else {
165
+ renderError(body.error || ("Couldn't load apps (HTTP " + res.status + ")."));
166
+ }
167
+ } catch (e) {
168
+ renderError("Couldn't load apps: " + (e && e.message ? e.message : e));
169
+ }
170
+ }
171
+
172
+ loadApps();
173
+ })();