@tokenoftrust/storefront-runner 1.3.4-rc.4 → 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.
Files changed (224) 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/meta/0000_snapshot.json +843 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
  10. package/apps/storefront/package.json +13 -1
  11. package/apps/storefront/perf/README.md +64 -0
  12. package/apps/storefront/perf/assert-budgets.ts +159 -0
  13. package/apps/storefront/playwright.config.ts +23 -0
  14. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  15. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  16. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  17. package/apps/storefront/src/components/ProductCard.astro +1 -0
  18. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
  19. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  20. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  21. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  22. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  23. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  24. package/apps/storefront/src/components/content/Callout.astro +75 -0
  25. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  26. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  27. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  28. package/apps/storefront/src/components/content/RichText.astro +44 -0
  29. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  30. package/apps/storefront/src/components/home/Hero.astro +14 -0
  31. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  32. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  33. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  34. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  35. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  36. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  37. package/apps/storefront/src/config/storeName.ts +34 -0
  38. package/apps/storefront/src/layouts/Layout.astro +97 -23
  39. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  40. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  41. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  42. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  43. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  44. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  45. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  46. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  47. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  48. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  49. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  50. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  51. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  52. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  53. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  54. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  55. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  56. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  57. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  58. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  59. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  60. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  61. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  62. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  63. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  64. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  65. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  66. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  67. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  68. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  69. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  70. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  71. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
  72. package/apps/storefront/src/lib/auth/route.ts +16 -1
  73. package/apps/storefront/src/lib/auth/session.ts +8 -0
  74. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  75. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  76. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  77. package/apps/storefront/src/lib/blog/types.ts +26 -0
  78. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  79. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  80. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  81. package/apps/storefront/src/lib/content/callout.ts +78 -0
  82. package/apps/storefront/src/lib/content/index.ts +27 -0
  83. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  84. package/apps/storefront/src/lib/content/prose.ts +186 -0
  85. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  86. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  87. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  88. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  89. package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
  90. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  91. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  92. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  93. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  94. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  95. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  96. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  97. package/apps/storefront/src/lib/env.ts +12 -0
  98. package/apps/storefront/src/lib/jsonld.ts +12 -17
  99. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  100. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  101. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  102. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  103. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  104. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  105. package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
  106. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  107. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  108. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  109. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  110. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  111. package/apps/storefront/src/lib/tot/query.ts +32 -0
  112. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  113. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  114. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  115. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  116. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  117. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  118. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  119. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  120. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  121. package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
  122. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
  123. package/apps/storefront/src/middleware/index.ts +28 -13
  124. package/apps/storefront/src/pages/404.astro +21 -9
  125. package/apps/storefront/src/pages/[...slug].astro +36 -2
  126. package/apps/storefront/src/pages/admin.astro +29 -3
  127. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
  128. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  129. package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
  130. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  131. package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
  132. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
  133. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
  134. package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
  135. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
  136. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  137. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
  138. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  139. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
  140. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  141. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  142. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  143. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  144. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  145. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  146. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  147. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
  148. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  149. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  150. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  151. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  152. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  153. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  154. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  155. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  156. package/apps/storefront/src/pages/auth/login.astro +197 -41
  157. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  158. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  159. package/apps/storefront/src/pages/blog/index.astro +98 -0
  160. package/apps/storefront/src/pages/capabilities.astro +8 -0
  161. package/apps/storefront/src/pages/cockpit.astro +433 -69
  162. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  163. package/apps/storefront/src/pages/collections/index.astro +10 -2
  164. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  165. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  166. package/apps/storefront/src/pages/index.astro +57 -0
  167. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  168. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  169. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  170. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  171. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  172. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  173. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  174. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  175. package/apps/storefront/src/styles/fonts.css +54 -0
  176. package/apps/storefront/src/styles/global.css +22 -2
  177. package/apps/storefront/src/themes/schema.ts +3 -25
  178. package/apps/storefront/tsconfig.json +1 -1
  179. package/apps/storefront/vitest.config.ts +4 -1
  180. package/package.json +1 -1
  181. package/packages/public-runtime/src/candidate-index.ts +311 -0
  182. package/packages/public-runtime/src/checkout.ts +94 -2
  183. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  184. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  185. package/packages/public-runtime/src/compliance/index.ts +22 -0
  186. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  187. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  188. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  189. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  190. package/packages/public-runtime/src/csp.ts +27 -3
  191. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  192. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  193. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  194. package/packages/public-runtime/src/extension-contract.ts +2 -1
  195. package/packages/public-runtime/src/hash.ts +25 -0
  196. package/packages/public-runtime/src/index.ts +6 -0
  197. package/packages/public-runtime/src/membership.ts +353 -0
  198. package/packages/public-runtime/src/product.ts +24 -2
  199. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  200. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  201. package/packages/public-runtime/src/tenant.ts +236 -0
  202. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  203. package/scripts/dev/publish.mjs +158 -0
  204. package/scripts/dev/transient-files.mjs +2 -1
  205. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  206. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  207. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  208. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  209. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  210. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  211. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  212. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  213. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  214. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  215. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  216. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  217. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  218. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  219. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  220. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  221. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  222. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  223. package/tenants/home/public/pages/storefront.css +23 -0
  224. package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
@@ -5,6 +5,9 @@
5
5
  * Every page renders inside this.
6
6
  */
7
7
  import "@/styles/global.css";
8
+ // Self-hosted web fonts (audit finding 6) — replaces the render-blocking
9
+ // fonts.googleapis.com stylesheet. Bundled by Vite, served same-origin.
10
+ import "@/styles/fonts.css";
8
11
  import Seo from "@/components/Seo.astro";
9
12
  import AnnouncementBar from "@/components/chrome/AnnouncementBar.astro";
10
13
  import SiteHeader from "@/components/chrome/SiteHeader.astro";
@@ -17,10 +20,13 @@ import ShippingRestrictionNotice from "@/components/compliance/ShippingRestricti
17
20
  import IsolatedAgeGate from "@/components/islands/IsolatedAgeGate.tsx";
18
21
  import AuthBadge from "@/components/auth/AuthBadge.astro";
19
22
  import { referenceTheme } from "@/themes/reference";
20
- import { mergeTheme, themeFontHref } from "@/themes/schema";
23
+ import { mergeTheme } from "@/themes/schema";
21
24
  import { organizationLd, websiteLd } from "@/lib/jsonld";
22
25
  import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
23
26
  import { readViewerSession } from "@/lib/auth/route";
27
+ import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
28
+ import { getRegistryService } from "@/lib/apps/adminService.js";
29
+ import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
24
30
 
25
31
  interface Props {
26
32
  title: string;
@@ -49,7 +55,6 @@ const allJsonLd = [
49
55
  ];
50
56
 
51
57
  const theme = mergeTheme(referenceTheme, tenant.theme_tokens);
52
- const fontHref = themeFontHref(theme);
53
58
 
54
59
  const chrome = await Astro.locals.content.getChrome();
55
60
 
@@ -92,12 +97,22 @@ const compliance = tenant.compliance;
92
97
  // open hosts where the gate never ran. Signed-out "Sign in" carries the current
93
98
  // path as `next`, so after the email-code round-trip the visitor lands back
94
99
  // where they were instead of the site root.
95
- const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
96
- const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
97
- const authLink = {
98
- label: viewer ? "Dashboard" : "Sign in",
99
- href: viewer ? "/dashboard" : `/auth/login?next=${nextParam}`,
100
- };
100
+ //
101
+ // Marketing tenants (siteType:"marketing" → features.catalog === false) have no
102
+ // shopper/vendor account surface to link to: /auth/login + /dashboard are also
103
+ // excluded outright from the static-publish route set (routeClassifier's
104
+ // auth/+dashboard/ prefixes), so a marketing-only site can never resolve either
105
+ // URL. Skip the affordance rather than render a dangling link — mirrors the
106
+ // same commerce/marketing split index.astro already keys off `!features.catalog`.
107
+ let authLink: { label: string; href: string } | undefined;
108
+ if (features.catalog) {
109
+ const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
110
+ const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
111
+ authLink = {
112
+ label: viewer ? "Dashboard" : "Sign in",
113
+ href: viewer ? "/dashboard" : `/auth/login?next=${nextParam}`,
114
+ };
115
+ }
101
116
 
102
117
  // Per-vendor header treatment from the theme file: a "dark" header renders the
103
118
  // bar in the primary color carrying the light logo (e.g. Giant Vapes' navy +
@@ -121,6 +136,23 @@ const legalLine = `© ${new Date().getFullYear()} ${tenant.displayName}${
121
136
  const analyticsToken = import.meta.env.PUBLIC_CF_ANALYTICS_TOKEN;
122
137
  const analyticsEnabled =
123
138
  analyticsToken && !/REPLACE|PLACEHOLDER/i.test(analyticsToken);
139
+
140
+ // Private-app widgets (D5) — global.footer slot. Resolved once here so it
141
+ // renders for BOTH footer paths below (the tenant-owned chrome.html splice
142
+ // and the generic <Footer>) instead of being owned by either. No dark-mode
143
+ // preference infra exists yet — "light" is the only theme hint until it does.
144
+ const footerWidgets = await renderWidgetSlot(
145
+ {
146
+ registry: await getRegistryService(),
147
+ tenantId: tenant.tenant_id,
148
+ customizationEnv: Astro.locals.customizationEnv,
149
+ slot: "global.footer",
150
+ route: path,
151
+ locale: tenant.locale,
152
+ theme: "light",
153
+ },
154
+ Astro.locals.activeWidgetPlacements,
155
+ );
124
156
  ---
125
157
 
126
158
  <!doctype html>
@@ -130,27 +162,20 @@ const analyticsEnabled =
130
162
  <meta name="viewport" content="width=device-width, initial-scale=1" />
131
163
  <link rel="icon" type="image/svg+xml" href={theme.brand.favicon} />
132
164
 
133
- {/* Per-tenant theme tokens injected at the edge (brief §7). */}
134
- <style set:html={themeStyle} nonce={cspNonce}></style>
165
+ {/* Per-tenant theme tokens injected at the edge (brief §7). The id is the
166
+ dev-loop hot-patch target: tenant-hot-reload.mjs recomputes this same
167
+ `:root{...}` block on a theme.json save and the script below swaps its
168
+ textContent in place — no full reload. Inert in production (no listener
169
+ attaches there). */}
170
+ <style id="tot-theme-vars" set:html={themeStyle} nonce={cspNonce}></style>
135
171
 
136
172
  {/* Shared chrome's own stylesheet(s) — the tenant theme (/tenants/...) plus
137
173
  either the tenant's own mkt.css or the platform /shared/commerce-chrome.css
138
174
  — so the ported chrome renders in-brand on Layout (commerce) pages too. */}
139
175
  {sharedChrome?.css.map((href) => <link rel="stylesheet" href={href} />)}
140
176
 
141
- {
142
- fontHref && (
143
- <>
144
- <link rel="preconnect" href="https://fonts.googleapis.com" />
145
- <link
146
- rel="preconnect"
147
- href="https://fonts.gstatic.com"
148
- crossorigin
149
- />
150
- <link rel="stylesheet" href={fontHref} />
151
- </>
152
- )
153
- }
177
+ {/* Web fonts are self-hosted (audit finding 6) via `@/styles/fonts.css`,
178
+ imported above — no third-party font stylesheet / preconnects. */}
154
179
 
155
180
  <Seo
156
181
  title={title}
@@ -230,6 +255,20 @@ const analyticsEnabled =
230
255
  </>
231
256
  )}
232
257
 
258
+ {/* Private-app widgets (D5, global.footer) — sandboxed per install; one
259
+ resolution above covers both footer paths. */}
260
+ {footerWidgets.map((w) => (
261
+ <div class="container-prose py-6">
262
+ <AppWidgetFrame
263
+ endpoint={w.endpoint}
264
+ launchToken={w.launchToken}
265
+ appOrigin={w.appOrigin}
266
+ installId={w.installId}
267
+ title={w.title ?? "App widget"}
268
+ />
269
+ </div>
270
+ ))}
271
+
233
272
  {capabilities.ageVerification.enabled && compliance?.minAge && (
234
273
  <IsolatedAgeGate client:load minAge={compliance.minAge} brand={tenant.displayName} />
235
274
  )}
@@ -342,6 +381,31 @@ const analyticsEnabled =
342
381
  </script>
343
382
  )}
344
383
 
384
+ {/* Dev-loop theme hot-patch (D1): tenant-hot-reload.mjs broadcasts a
385
+ `tot:theme-vars` HMR custom event on every theme.json save instead of
386
+ forcing a full reload; swap the CSS var block in place so edits repaint
387
+ instantly with no lost scroll position. import.meta.hot is undefined in
388
+ production builds, so this is a no-op there. */}
389
+ {import.meta.env.DEV && (
390
+ <script>
391
+ if (import.meta.hot) {
392
+ import.meta.hot.on("tot:theme-vars", (data: { css?: string }) => {
393
+ const el = document.getElementById("tot-theme-vars");
394
+ if (el && typeof data?.css === "string") el.textContent = data.css;
395
+ });
396
+ }
397
+ </script>
398
+ )}
399
+
400
+ {/* Private-app widget postMessage bridge (D5) — mounted unconditionally
401
+ (cheap no-op with zero tracked iframes); scans for
402
+ `[data-tot-widget-origin]` frames itself, so it doesn't need to know
403
+ which slots rendered above. */}
404
+ <script>
405
+ import { mountWidgetPostMessageBridge } from "@tot/public-runtime";
406
+ mountWidgetPostMessageBridge();
407
+ </script>
408
+
345
409
  {/* Scroll-reveal + analytics pageview hook. ~0.5KB, no framework. */}
346
410
  <script>
347
411
  // Reveal-on-scroll (respects reduced motion via CSS).
@@ -366,5 +430,15 @@ const analyticsEnabled =
366
430
  els.forEach((el) => el.classList.add("is-in"));
367
431
  }
368
432
  </script>
433
+
434
+ {/* Field Web Vitals (F14 RUM) — measures LCP/INP/CLS for real visitors and
435
+ beacons per-route samples to the RUM sink. ~1KB, no framework; no-ops
436
+ where PerformanceObserver is absent. This is the per-route-template
437
+ signal F16's `core-web-vitals` monitor consumes (the CF Web Analytics
438
+ beacon above only gives aggregate field CWV). */}
439
+ <script>
440
+ import { startWebVitals } from "@/lib/analytics/webVitals";
441
+ startWebVitals();
442
+ </script>
369
443
  </body>
370
444
  </html>
@@ -0,0 +1,69 @@
1
+ {
2
+ "$comment": "F14 performance budgets manifest — the checked-in, tool-neutral source of truth for the storefront go-live framework's perf gate. Consumed by (1) the lab budget-assertion harness (perf/assert-budgets.ts), (2) the Lighthouse budgets exporter (toLighthouseBudgets), and (3) the field RUM regression-alert contract (rumAlert.ts). Edit HERE — everything else derives from it. Shape + validation: src/lib/analytics/perfBudgets.ts (parsePerfBudgets).",
3
+ "version": 1,
4
+ "webVitals": {
5
+ "$comment": "Core Web Vitals thresholds, evaluated at the 75th percentile (p75) of the field distribution — the same percentile Google's CrUX / the RUM alert contract use.",
6
+ "lcpMs": 2500,
7
+ "inpMs": 200,
8
+ "cls": 0.1
9
+ },
10
+ "resourceBudgets": {
11
+ "$comment": "Per-page transfer budgets in kibibytes (KiB), compressed-over-the-wire. Defaults; a route may tighten or (rarely) loosen them via its own resourceBudgets.",
12
+ "jsKib": 180,
13
+ "cssKib": 90,
14
+ "imageKib": 900,
15
+ "totalKib": 1300
16
+ },
17
+ "thirdPartyScriptAllowlist": [
18
+ "static.cloudflareinsights.com",
19
+ "cdn.foxycart.com"
20
+ ],
21
+ "routes": [
22
+ {
23
+ "id": "home",
24
+ "label": "Home",
25
+ "samplePath": "/",
26
+ "match": ["="]
27
+ },
28
+ {
29
+ "id": "plp",
30
+ "label": "Product listing",
31
+ "samplePath": "/collections/all",
32
+ "match": ["/collections"]
33
+ },
34
+ {
35
+ "id": "pdp",
36
+ "label": "Product detail",
37
+ "samplePath": "/products/",
38
+ "match": ["/products/"],
39
+ "resourceBudgets": {
40
+ "$comment": "PDP carries the largest hero + gallery imagery.",
41
+ "imageKib": 1100,
42
+ "totalKib": 1500
43
+ }
44
+ },
45
+ {
46
+ "id": "search",
47
+ "label": "Search results",
48
+ "samplePath": "/search?q=cap",
49
+ "match": ["/search"]
50
+ },
51
+ {
52
+ "id": "account",
53
+ "label": "Account / dashboard",
54
+ "samplePath": "/dashboard",
55
+ "match": ["/dashboard", "/account", "/saved"]
56
+ },
57
+ {
58
+ "id": "checkout",
59
+ "label": "Checkout entry",
60
+ "samplePath": "/cart",
61
+ "match": ["/cart", "/checkout"],
62
+ "resourceBudgets": {
63
+ "$comment": "The managed-checkout loader (cdn.foxycart.com) ships its own JS bundle; give this route headroom the on-site pages do not need.",
64
+ "jsKib": 320,
65
+ "totalKib": 1600
66
+ }
67
+ }
68
+ ]
69
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * F14 — Lighthouse result → budget measurement (the lab hook, pure half).
3
+ *
4
+ * The budget gate (`perfBudgets.ts`) evaluates a tool-neutral `RouteMeasurement`.
5
+ * This module is the one adapter that turns a Lighthouse result object (LHR JSON,
6
+ * as `lighthouse --output=json` / LHCI emit) into that shape, so a live
7
+ * Lighthouse run in preview validation feeds the SAME gate the unit tests do.
8
+ *
9
+ * Kept pure (LHR in → measurement out, no fs/network) so `lighthouseReport.test.ts`
10
+ * can prove the extraction against a fixture LHR without running Lighthouse; the
11
+ * thin CLI (`perf/assert-budgets.ts`) does the fs + Lighthouse invocation.
12
+ *
13
+ * We read only the stable, documented LHR fields:
14
+ * - `audits['largest-contentful-paint'].numericValue` (ms) → webVitals.lcpMs
15
+ * - `audits['cumulative-layout-shift'].numericValue` (unitless)→ webVitals.cls
16
+ * - `audits['resource-summary'].details.items[]` (bytes) → resource KiB
17
+ * - `audits['network-requests'].details.items[]` (url) → third-party origins
18
+ * INP is intentionally absent — it has no lab value; it is enforced from field
19
+ * RUM (`rumAlert.ts`), matching `toLighthouseBudgets`' note.
20
+ *
21
+ * Third-party origins are taken from `network-requests` (real request URLs →
22
+ * hostnames), NOT `third-party-summary` — the latter reports entity DISPLAY
23
+ * names ("Google Fonts"), which would never match the manifest's hostname
24
+ * allowlist. We keep hostnames that loaded a Script and are cross-origin to the
25
+ * page, so the gate's allowlist check compares like-for-like.
26
+ */
27
+ import type { RouteId, RouteMeasurement } from "./perfBudgets.js";
28
+
29
+ /** The (loose) slice of a Lighthouse result we read. */
30
+ export interface LighthouseResult {
31
+ audits?: Record<
32
+ string,
33
+ {
34
+ numericValue?: number;
35
+ details?: {
36
+ items?: Array<Record<string, unknown>>;
37
+ };
38
+ }
39
+ >;
40
+ finalUrl?: string;
41
+ requestedUrl?: string;
42
+ }
43
+
44
+ const hostnameOf = (url: string | undefined): string | undefined => {
45
+ if (!url) return undefined;
46
+ try {
47
+ return new URL(url).hostname.toLowerCase();
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ };
52
+
53
+ const BYTES_PER_KIB = 1024;
54
+ const toKib = (bytes: unknown): number | undefined =>
55
+ typeof bytes === "number" && Number.isFinite(bytes)
56
+ ? Math.round((bytes / BYTES_PER_KIB) * 10) / 10
57
+ : undefined;
58
+
59
+ /** Map a Lighthouse `resource-summary` resourceType onto our budget keys. */
60
+ const RESOURCE_TYPE_KEY: Record<string, "jsKib" | "cssKib" | "imageKib" | "totalKib"> = {
61
+ script: "jsKib",
62
+ stylesheet: "cssKib",
63
+ image: "imageKib",
64
+ total: "totalKib",
65
+ };
66
+
67
+ /**
68
+ * Extract a `RouteMeasurement` for `routeId` from a Lighthouse result. Missing
69
+ * audits simply yield missing dimensions — the gate only checks what was
70
+ * measured — so a partial/failed audit under-reports rather than crashing.
71
+ */
72
+ export function lhrToMeasurement(lhr: LighthouseResult, routeId: RouteId): RouteMeasurement {
73
+ const audits = lhr.audits ?? {};
74
+
75
+ const resources: NonNullable<RouteMeasurement["resources"]> = {};
76
+ const summaryItems = audits["resource-summary"]?.details?.items ?? [];
77
+ for (const item of summaryItems) {
78
+ const type = typeof item["resourceType"] === "string" ? (item["resourceType"] as string) : "";
79
+ const key = RESOURCE_TYPE_KEY[type];
80
+ const kib = toKib(item["transferSize"]);
81
+ if (key && kib !== undefined) resources[key] = kib;
82
+ }
83
+
84
+ const webVitals: NonNullable<RouteMeasurement["webVitals"]> = {};
85
+ const lcp = audits["largest-contentful-paint"]?.numericValue;
86
+ if (typeof lcp === "number") webVitals.lcpMs = Math.round(lcp);
87
+ const cls = audits["cumulative-layout-shift"]?.numericValue;
88
+ if (typeof cls === "number") webVitals.cls = Math.round(cls * 1e4) / 1e4;
89
+
90
+ // Third-party SCRIPT hostnames, cross-origin to the page, from real requests.
91
+ const pageHost = hostnameOf(lhr.finalUrl ?? lhr.requestedUrl);
92
+ const netItems = audits["network-requests"]?.details?.items ?? [];
93
+ const thirdPartyOrigins: string[] = [];
94
+ for (const item of netItems) {
95
+ const type = typeof item["resourceType"] === "string" ? (item["resourceType"] as string) : "";
96
+ if (type.toLowerCase() !== "script") continue;
97
+ const host = hostnameOf(typeof item["url"] === "string" ? (item["url"] as string) : undefined);
98
+ if (!host || host === pageHost) continue;
99
+ if (!thirdPartyOrigins.includes(host)) thirdPartyOrigins.push(host);
100
+ }
101
+
102
+ return {
103
+ routeId,
104
+ ...(lhr.finalUrl || lhr.requestedUrl ? { url: lhr.finalUrl ?? lhr.requestedUrl } : {}),
105
+ ...(Object.keys(resources).length ? { resources } : {}),
106
+ ...(Object.keys(webVitals).length ? { webVitals } : {}),
107
+ ...(thirdPartyOrigins.length ? { thirdPartyOrigins } : {}),
108
+ };
109
+ }