@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.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.
- package/apps/storefront/astro.config.mjs +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -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
|
|
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,8 +19,16 @@
|
|
|
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",
|
|
31
|
+
"@tokenoftrust/private-apps-devkit": "*",
|
|
24
32
|
"@tot/public-runtime": "*",
|
|
25
33
|
"astro": "^7.0.3",
|
|
26
34
|
"fuse.js": "^7.4.2",
|
|
@@ -31,9 +39,13 @@
|
|
|
31
39
|
},
|
|
32
40
|
"devDependencies": {
|
|
33
41
|
"@astrojs/check": "^0.9.4",
|
|
42
|
+
"@axe-core/playwright": "^4.12.1",
|
|
43
|
+
"@playwright/test": "^1.61.1",
|
|
44
|
+
"axe-core": "^4.12.1",
|
|
34
45
|
"dependency-cruiser": "^18.0.0",
|
|
35
46
|
"drizzle-kit": "^0.31.10",
|
|
36
47
|
"drizzle-orm": "^0.45.2",
|
|
48
|
+
"tsx": "^4.22.4",
|
|
37
49
|
"typescript": "^5.6.3",
|
|
38
50
|
"vitest": "2.1.8",
|
|
39
51
|
"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
|
+
})();
|