@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,150 @@
1
+ /**
2
+ * Shared emit helper for UI-ORIGINATED operational activity (card D6).
3
+ *
4
+ * The UI cohort's entry point onto the D0 actor×action telemetry contract. It sits
5
+ * ON TOP of D1's server emitter ({@link recordActivity}) — every admin/cockpit action
6
+ * handler already runs SERVER-SIDE in the same Worker, so a UI action reaches
7
+ * telemetry through the same in-process `recordActivity()` call D1 uses (one
8
+ * structured Workers-Logs line), NOT a client-side `fetch` to the D4 ingest endpoint.
9
+ * There is no browser-only signal to capture: an admin/cockpit "Publish / Connect /
10
+ * Invite / Approve / Rollback" click is a server-rendered form/JSON POST to an API
11
+ * route, so the route handler that already does the work is exactly where the emit
12
+ * belongs (zero extra HTTP round-trip, no new auth/trust boundary). The D4 HMAC
13
+ * ingest path exists for emitters that CAN'T call `recordActivity()` directly (the
14
+ * out-of-process CLI cohort, D3) — it is not this cohort's path.
15
+ *
16
+ * WHAT THIS ADDS over calling `recordActivity()` raw: it stamps `source: "ui"` and,
17
+ * critically, resolves the ACTOR KIND from the authenticated session so the timeline
18
+ * (D5) can attribute an action to the right cohort:
19
+ *
20
+ * • "admin" — a ToT-STAFF viewer (the session carries `staff[]` scope). ToT
21
+ * operators acting through the admin shell / staff vendor selection.
22
+ * • "merchant" — a store OWNER acting on THEIR OWN store (a signed-in viewer whose
23
+ * host-bound capability is owner/admin). The self-serve cohort.
24
+ * • "dev" — a signed-in DEVELOPER viewer who is neither staff nor owner (e.g. a
25
+ * ship-on-behalf developer invited onto someone else's store).
26
+ * • "agent" — a HEADLESS, capability-driven caller with no human viewer session
27
+ * (the Bearer/CLI operator seam) — automation acting on the surface.
28
+ *
29
+ * The classifier is a PURE function ({@link classifyActorKind}) so the cohort mapping
30
+ * is unit-tested without a live request. The opaque `actor.id` is the salted,
31
+ * non-reversible hash of the acting identity (never raw PII in the core zone) — the
32
+ * SAME {@link hashActorId} + `ACTIVITY_ACTOR_SALT` D1 established, so one person stays
33
+ * correlatable across the CLI, server, and UI cohorts.
34
+ *
35
+ * USAGE (mirrors D1's reject.ts — resolve the actor ONCE after auth, emit at each
36
+ * exit point, zero change to the handler's control flow):
37
+ *
38
+ * const actor = await resolveUiActor(context, { owner: auth.owner });
39
+ * // …do the work…
40
+ * recordUiActivity(actor, { action: "change.ship", outcome: { status: "succeeded" }, scope });
41
+ */
42
+ import type { APIContext } from "astro";
43
+ import type {
44
+ ActivityActor,
45
+ ActorKind,
46
+ ActivityOutcome,
47
+ ActivityScope,
48
+ ActivityPayload,
49
+ } from "@tot/public-runtime";
50
+ import type { ActionKey } from "@tot/public-runtime";
51
+ import { readViewerSession } from "@/lib/auth/route";
52
+ import { isStaffSession } from "@/lib/dashboard/staffAdmission";
53
+ import { readEnv } from "@/lib/env";
54
+ import { hashActorId, recordActivity } from "./recordActivity";
55
+
56
+ /** The host-bound capabilities that mark a viewer as the store OWNER (merchant). */
57
+ const OWNER_CAPABILITIES = new Set(["owner", "admin"]);
58
+
59
+ /** The minimal viewer chrome the middleware stamps on `locals.viewer`. */
60
+ interface ViewerLike {
61
+ email?: string;
62
+ capability?: string;
63
+ }
64
+
65
+ /**
66
+ * Classify the acting cohort from the authenticated session — PURE so the mapping is
67
+ * testable without a request. Order matters and is fail-safe:
68
+ *
69
+ * 1. no signed-in viewer → "agent" (headless Bearer/CLI operator — capability-
70
+ * driven automation; there is no human identity on this request).
71
+ * 2. ToT-staff session → "admin" (checked BEFORE ownership: a staff viewer who
72
+ * entered a vendor via a live selection carries an owner-ish host capability, but
73
+ * they are ToT staff, not the merchant).
74
+ * 3. owner/admin capability on this host → "merchant" (the store owner on their own store).
75
+ * 4. otherwise → "dev" (a signed-in developer without ownership, e.g.
76
+ * ship-on-behalf).
77
+ */
78
+ export function classifyActorKind(
79
+ viewer: ViewerLike | null | undefined,
80
+ session: { staff?: string[] } | null | undefined,
81
+ ): ActorKind {
82
+ if (!viewer?.email) return "agent";
83
+ if (isStaffSession(session)) return "admin";
84
+ if (viewer.capability && OWNER_CAPABILITIES.has(viewer.capability)) return "merchant";
85
+ return "dev";
86
+ }
87
+
88
+ export interface ResolveUiActorInput {
89
+ /**
90
+ * Fallback acting identity for the HEADLESS path (no signed-in viewer) — typically
91
+ * the resolved tenant owner (`auth.owner`). Only used to seed the opaque id when
92
+ * there is no viewer email; never stored raw (always hashed).
93
+ */
94
+ owner?: string;
95
+ }
96
+
97
+ /**
98
+ * Resolve the {@link ActivityActor} for a UI-originated action from the request's
99
+ * authenticated session. Reads the full viewer session ONCE (for the staff scope the
100
+ * host-bound `locals.viewer` chrome doesn't carry) and computes the opaque, salted
101
+ * actor id. Best-effort: any read fault degrades to a coarse non-PII actor rather
102
+ * than throwing — telemetry must never break the action it describes. Resolve this
103
+ * ONCE per request (after auth) and reuse the result across every emit exit point.
104
+ */
105
+ export async function resolveUiActor(
106
+ context: APIContext,
107
+ input: ResolveUiActorInput = {},
108
+ ): Promise<ActivityActor> {
109
+ // The host-bound `locals.viewer` chrome omits `staff[]`, so read the full session
110
+ // to tell a ToT-staff (admin) actor from a store owner (merchant). Best-effort.
111
+ const session = await readViewerSession(context).catch(() => null);
112
+ // Prefer the host-scoped `locals.viewer` (set by middleware for tenant-gated
113
+ // surfaces — the ship/grants routes); fall back to the raw session's
114
+ // email/capability for a route that never went through that host gate (e.g. the
115
+ // /dev cockpit's rendezvous-approval endpoint) but still has a real signed-in
116
+ // viewer. Both describe the SAME signed-in-or-not shape.
117
+ const viewer: ViewerLike | undefined =
118
+ (context.locals?.viewer as ViewerLike | undefined) ??
119
+ (session ? { email: session.email, capability: session.capability } : undefined);
120
+ const kind = classifyActorKind(viewer, session);
121
+ const identifier = viewer?.email ?? input.owner ?? "system";
122
+ const id = await hashActorId(identifier, await readEnv("ACTIVITY_ACTOR_SALT"));
123
+ return { kind, id };
124
+ }
125
+
126
+ /**
127
+ * Emit ONE UI-originated activity event with a PRE-RESOLVED actor. A thin, SYNC
128
+ * wrapper over D1's {@link recordActivity} that stamps `source: "ui"` — so a handler
129
+ * calls it at each exit point (succeeded / failed / refused) exactly as D1's server
130
+ * emitters do, with no control-flow change. Best-effort + never throws (inherited
131
+ * from `recordActivity`).
132
+ */
133
+ export function recordUiActivity(
134
+ actor: ActivityActor,
135
+ input: {
136
+ action: ActionKey;
137
+ outcome: ActivityOutcome;
138
+ scope?: ActivityScope;
139
+ payload?: ActivityPayload;
140
+ },
141
+ ): void {
142
+ recordActivity({
143
+ action: input.action,
144
+ actor,
145
+ source: "ui",
146
+ outcome: input.outcome,
147
+ ...(input.scope ? { scope: input.scope } : {}),
148
+ ...(input.payload ? { payload: input.payload } : {}),
149
+ });
150
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * worker_commit — build provenance for the running Worker (card D2).
3
+ *
4
+ * The commit SHA THIS Worker build was cut from, made available REPO-WIDE at
5
+ * runtime so every server-emitted ActivityEvent can carry it (see event.ts
6
+ * `ActivityScope.workerCommit`) and D5 can later derive **deploy skew** — "is the
7
+ * build serving this request == the pushed umbrella tip, or is a stale worker
8
+ * still live?".
9
+ *
10
+ * HOW IT GETS HERE. Workers have no filesystem/git at runtime, so the SHA is
11
+ * baked in at BUILD time: astro.config.mjs's Vite `define` replaces the
12
+ * `__GIT_SHA__` token with a string literal of the build's commit (CI-provided
13
+ * `GIT_SHA`/`WORKERS_CI_COMMIT_SHA`/`GITHUB_SHA`, else local `git rev-parse`,
14
+ * else "unknown"). This is the SAME source /health already reports (lib/health.ts)
15
+ * — we read it through the identical `typeof` guard so both agree by construction,
16
+ * and so the value is safe under vitest (where the define isn't applied → "unknown").
17
+ *
18
+ * This is the single accessor the whole repo threads through: D1's recordActivity()
19
+ * spreads serverActivityScope() into every server event; D2's deploy.version_observed
20
+ * emitter (deployVersion.ts) reads workerCommit() directly.
21
+ */
22
+ import type { ActivityScope } from "@tot/public-runtime";
23
+
24
+ /** Sentinel when the build had no git context (also the vitest value). */
25
+ export const UNKNOWN_COMMIT = "unknown";
26
+
27
+ /**
28
+ * The commit SHA this Worker build was cut from, or "unknown". Never throws; the
29
+ * `typeof` guard keeps it safe under vitest (the `__GIT_SHA__` define is only
30
+ * applied by the Astro/Vite build). Optionally overridable for tests.
31
+ */
32
+ export function workerCommit(override?: string): string {
33
+ if (override != null) return override;
34
+ return typeof __GIT_SHA__ !== "undefined" ? __GIT_SHA__ : UNKNOWN_COMMIT;
35
+ }
36
+
37
+ /** First 7 chars of the build commit for eyeballing against `git log`, or "unknown". */
38
+ export function workerCommitShort(override?: string): string {
39
+ const sha = workerCommit(override);
40
+ return sha === UNKNOWN_COMMIT ? UNKNOWN_COMMIT : sha.slice(0, 7);
41
+ }
42
+
43
+ /**
44
+ * True when the build has a real git commit (not the "unknown" sentinel). D5's
45
+ * skew derivation only compares against the umbrella tip when this holds — an
46
+ * "unknown" build can't be meaningfully skew-checked.
47
+ */
48
+ export function hasKnownWorkerCommit(override?: string): boolean {
49
+ return workerCommit(override) !== UNKNOWN_COMMIT;
50
+ }
51
+
52
+ /**
53
+ * Build the CORE scope for a SERVER-sourced ActivityEvent with `worker_commit`
54
+ * stamped in. This is the seam D1's recordActivity() spreads into every server
55
+ * event so provenance is present WITHOUT each call site remembering it:
56
+ *
57
+ * createActivityEvent({ ..., source: "server", scope: serverActivityScope({ tenantId }) })
58
+ *
59
+ * Pass the event's other scope fields (tenantId/changeId/traceId) as `extra`;
60
+ * workerCommit always wins for the `workerCommit` slot. Omits the field entirely
61
+ * when the build commit is "unknown" so a downstream skew query never mistakes the
62
+ * sentinel for a real deployed SHA.
63
+ */
64
+ export function serverActivityScope(extra?: ActivityScope): ActivityScope {
65
+ const sha = workerCommit();
66
+ return {
67
+ ...extra,
68
+ ...(sha !== UNKNOWN_COMMIT ? { workerCommit: sha } : {}),
69
+ };
70
+ }
@@ -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
+ }