@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
@@ -0,0 +1,206 @@
1
+ /**
2
+ * POST /api/compliance/preflight — the checkout ENFORCEMENT gate.
3
+ *
4
+ * Checkout completion + order creation happen in the EXTERNAL managed cart, so
5
+ * this route is the storefront's authoritative "may this cart proceed?" decision,
6
+ * evaluated SERVER-SIDE against the tenant's data-driven ruleset. The client posts
7
+ * the cart handles + a ship-to state + the verification state; the server resolves
8
+ * each product's compliance profile from the catalog (never trusting client-
9
+ * supplied attributes), evaluates, and returns a structured decision.
10
+ *
11
+ * Fail-closed by construction: a regulated tenant with no resolvable ruleset, an
12
+ * unconfigured verification provider, or an unsatisfied excise obligation returns
13
+ * `status: "blocked"`. A malformed body on a regulated tenant blocks too — it
14
+ * never falls through to "clear".
15
+ */
16
+ import type { APIContext } from "astro";
17
+ import {
18
+ deriveProductProfile,
19
+ evaluateCartCompliance,
20
+ type CartComplianceLine,
21
+ type ShipDestination,
22
+ type VerificationState,
23
+ type CartComplianceDecision,
24
+ } from "@tot/public-runtime";
25
+ import {
26
+ isRegulatedTenant,
27
+ isSandboxVerificationProvider,
28
+ resolveTenantRuleset,
29
+ } from "@/lib/compliance/enforcement";
30
+
31
+ export const prerender = false;
32
+
33
+ interface PreflightLineInput {
34
+ handle: string;
35
+ sku?: string;
36
+ variantId?: string;
37
+ quantity?: number;
38
+ }
39
+
40
+ interface PreflightBody {
41
+ lines?: PreflightLineInput[];
42
+ destination?: { region?: string; country?: string; postalCode?: string } | null;
43
+ verification?: {
44
+ outcome?: string;
45
+ verifiedAge?: number;
46
+ evidenceRef?: string;
47
+ } | null;
48
+ ageAffirmed?: boolean;
49
+ }
50
+
51
+ function json(body: unknown, status = 200): Response {
52
+ return new Response(JSON.stringify(body), {
53
+ status,
54
+ headers: {
55
+ "content-type": "application/json; charset=utf-8",
56
+ "cache-control": "no-store",
57
+ },
58
+ });
59
+ }
60
+
61
+ /** A fail-closed BLOCK decision for regulated tenants when the request is unusable. */
62
+ function blockedDecision(message: string): CartComplianceDecision {
63
+ return {
64
+ status: "blocked",
65
+ blocks: [{ code: "ruleset_unavailable", failedClosed: true, message }],
66
+ requirements: {
67
+ ageVerification: "blocked",
68
+ adultSignature: false,
69
+ exciseHandling: "not_required",
70
+ destination: "not_required",
71
+ },
72
+ purchaseLimit: null,
73
+ failedClosed: true,
74
+ };
75
+ }
76
+
77
+ const VALID_OUTCOMES = new Set(["verified", "failed", "pending", "unknown"]);
78
+
79
+ function normalizeVerification(
80
+ v: PreflightBody["verification"],
81
+ ): VerificationState | null {
82
+ if (!v || typeof v !== "object") return null;
83
+ const outcome = VALID_OUTCOMES.has(String(v.outcome))
84
+ ? (v.outcome as VerificationState["outcome"])
85
+ : "unknown";
86
+ return {
87
+ outcome,
88
+ verifiedAge:
89
+ typeof v.verifiedAge === "number" ? v.verifiedAge : undefined,
90
+ evidenceRef:
91
+ typeof v.evidenceRef === "string" ? v.evidenceRef : undefined,
92
+ };
93
+ }
94
+
95
+ function normalizeDestination(
96
+ d: PreflightBody["destination"],
97
+ ): ShipDestination | null {
98
+ if (!d || typeof d !== "object") return null;
99
+ const region = String(d.region ?? "").trim();
100
+ const country = String(d.country ?? "US").trim() || "US";
101
+ if (!region) return null;
102
+ return {
103
+ region,
104
+ country,
105
+ postalCode:
106
+ typeof d.postalCode === "string" ? d.postalCode.trim() : undefined,
107
+ };
108
+ }
109
+
110
+ export async function POST(context: APIContext): Promise<Response> {
111
+ const { tenant, tot } = context.locals;
112
+ const regulated = isRegulatedTenant(tenant);
113
+
114
+ let body: PreflightBody;
115
+ try {
116
+ body = (await context.request.json()) as PreflightBody;
117
+ } catch {
118
+ // A regulated tenant must never proceed on an unreadable request.
119
+ return regulated
120
+ ? json({ decision: blockedDecision("Malformed preflight request.") }, 400)
121
+ : json({ decision: evaluateEmpty() });
122
+ }
123
+
124
+ const ruleset = await resolveTenantRuleset(tenant);
125
+ let verification = normalizeVerification(body.verification);
126
+ const destination = normalizeDestination(body.destination);
127
+
128
+ // Test/sandbox verification profile (the mode the 8501 demo/preview runs in):
129
+ // when the resolved provider is an explicit sandbox value and no live provider
130
+ // outcome was supplied, the front-door 21+ affirmation SIMULATES a passed
131
+ // verification so the regulated checkout is exercisable end-to-end without a
132
+ // real provider. Gated on the sandbox provider (test-tier only) — production
133
+ // never reaches here (unset provider → fail-closed; live provider → real
134
+ // outcome required). See enforcement.ts for the invariant.
135
+ if (
136
+ ruleset &&
137
+ isSandboxVerificationProvider(ruleset.verification.provider) &&
138
+ body.ageAffirmed === true &&
139
+ (!verification || verification.outcome === "unknown")
140
+ ) {
141
+ verification = { outcome: "verified", verifiedAge: ruleset.verification.minAge };
142
+ }
143
+
144
+ // Resolve each line's compliance profile SERVER-SIDE from the catalog.
145
+ const lines: CartComplianceLine[] = [];
146
+ for (const raw of body.lines ?? []) {
147
+ if (!raw || typeof raw.handle !== "string" || !raw.handle) continue;
148
+ const quantity =
149
+ typeof raw.quantity === "number" && raw.quantity > 0
150
+ ? Math.floor(raw.quantity)
151
+ : 1;
152
+ const product = await tot.getProductByHandle(raw.handle);
153
+ if (!product) {
154
+ // A line we can't resolve on a regulated tenant is a fail-closed block:
155
+ // we can't prove it's shippable, so we don't let it through.
156
+ if (regulated) {
157
+ return json(
158
+ {
159
+ decision: blockedDecision(
160
+ `Product "${raw.handle}" could not be resolved for compliance evaluation.`,
161
+ ),
162
+ },
163
+ 200,
164
+ );
165
+ }
166
+ continue;
167
+ }
168
+ const variant =
169
+ product.variants.find(
170
+ (v) => v.id === raw.variantId || (raw.sku && v.sku === raw.sku),
171
+ ) ?? null;
172
+ const profile = deriveProductProfile(product, variant);
173
+ lines.push({
174
+ sku: variant?.sku ?? raw.sku ?? product.handle,
175
+ quantity,
176
+ profile,
177
+ title: product.title,
178
+ });
179
+ }
180
+
181
+ const decision = evaluateCartCompliance({
182
+ ruleset,
183
+ regulated,
184
+ lines,
185
+ destination,
186
+ verification,
187
+ ageAffirmed: body.ageAffirmed === true,
188
+ });
189
+
190
+ return json({ decision });
191
+ }
192
+
193
+ /** The trivial "nothing to enforce" decision for an unregulated empty request. */
194
+ function evaluateEmpty(): CartComplianceDecision {
195
+ return evaluateCartCompliance({
196
+ ruleset: null,
197
+ regulated: false,
198
+ lines: [],
199
+ destination: null,
200
+ verification: null,
201
+ });
202
+ }
203
+
204
+ export async function GET(): Promise<Response> {
205
+ return json({ error: "method_not_allowed" }, 405);
206
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Field Web Vitals (F14 RUM) ingest — the sink the browser beacon POSTs to.
3
+ *
4
+ * `webVitals.ts#startWebVitals` sends one JSON `WebVitalsSample` here (via
5
+ * `navigator.sendBeacon`) as each Core Web Vital finalizes. This endpoint is the
6
+ * server side of that contract: validate the sample, record it, return 204.
7
+ *
8
+ * Phase 1 records to the worker log (like `csp-report.ts`). The regression alert
9
+ * (`rumAlert.detectRegressions`) runs over a WINDOW of samples, not one — so the
10
+ * durable step is to forward each sample to a queryable store (Workers Analytics
11
+ * Engine / a KV rollup), then run `detectRegressions` on the per-route/hour p75
12
+ * and dispatch any `RegressionAlert` to F16's `core-web-vitals` monitor. That
13
+ * store/forward is the documented follow-up; the SHAPE it stores is fixed here.
14
+ */
15
+ import type { APIRoute } from "astro";
16
+ import { rateMetric, type WebVitalName, type WebVitalsSample } from "@/lib/analytics/webVitals";
17
+
18
+ export const prerender = false;
19
+
20
+ const METRICS: readonly WebVitalName[] = ["lcp", "inp", "cls"];
21
+
22
+ /** Narrow an untrusted beacon body to a WebVitalsSample, or null if malformed. */
23
+ function parseSample(raw: unknown): WebVitalsSample | null {
24
+ if (!raw || typeof raw !== "object") return null;
25
+ const o = raw as Record<string, unknown>;
26
+ if (typeof o.metric !== "string" || !METRICS.includes(o.metric as WebVitalName)) return null;
27
+ if (typeof o.value !== "number" || !Number.isFinite(o.value) || o.value < 0) return null;
28
+ if (typeof o.route !== "string" || typeof o.path !== "string") return null;
29
+ const metric = o.metric as WebVitalName;
30
+ return {
31
+ metric,
32
+ value: o.value,
33
+ // Re-derive rating server-side — never trust the client's label.
34
+ rating: rateMetric(metric, o.value),
35
+ route: o.route as WebVitalsSample["route"],
36
+ path: o.path,
37
+ ...(typeof o.navigationType === "string" ? { navigationType: o.navigationType } : {}),
38
+ };
39
+ }
40
+
41
+ export const POST: APIRoute = async ({ request }) => {
42
+ try {
43
+ const sample = parseSample(await request.json());
44
+ if (sample) {
45
+ // eslint-disable-next-line no-console
46
+ console.info("[rum:web-vitals]", JSON.stringify(sample));
47
+ // TODO(F14 follow-up): forward to Analytics Engine and run
48
+ // rumAlert.detectRegressions on the per-route/hour rollup.
49
+ }
50
+ } catch {
51
+ /* never fail a telemetry beacon */
52
+ }
53
+ return new Response(null, { status: 204 });
54
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * POST /api/test/dev-session — E2E-ONLY hands-off sign-in for automated tests.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * The hosted /cockpit `hostedActivity` flow can only be driven by a signed-in,
7
+ * store-LINKED developer session, and the real sign-in paths (`/api/auth/verify`
8
+ * OTP code, `/auth/magic` token) deliver their secret ONLY by email — so an
9
+ * automated regression test can never bootstrap itself without a human relaying a
10
+ * code/link out of an inbox. This endpoint mints that session directly for a test
11
+ * developer + tenant so `e2e/cockpit-signal-rail.spec.ts` can run unattended.
12
+ *
13
+ * IT DOES NOT WEAKEN THE REAL AUTH FLOW. It is a SEPARATE, secret-gated door used
14
+ * only as a test PRECONDITION — auth-flow fidelity (does invite→email→magic→session
15
+ * actually work?) is a different concern and must still be tested against the real
16
+ * path with a real/test inbox. See e2e/README.md §"Why a mint endpoint, not a flag".
17
+ *
18
+ * SAFETY — fail CLOSED, three independent gates (a env-detection bug alone can't open it):
19
+ * 1. `E2E_TEST_SECRET` must be provisioned. Unset → 404 (route behaves as if it
20
+ * does not exist). PROVISION IT ONLY IN THE TEST/PREVIEW TIER, NEVER PROD.
21
+ * 2. `NODE_ENV === "production"` → 404 regardless of anything else (defense in depth;
22
+ * preview/qa run NODE_ENV=test, so this permits them and blocks prod).
23
+ * 3. The `x-e2e-secret` header must match `E2E_TEST_SECRET` (constant-time compare).
24
+ * Blast radius is further bounded: the tenant must resolve via resolvePreviewTenant
25
+ * (a known store), and the minted session/token are the ordinary revocable KV
26
+ * artifacts (deleting the KV entry revokes them) — nothing new or long-lived.
27
+ *
28
+ * This mirrors the existing shared-secret seam the storefront already uses for
29
+ * cli-signin-code (lib/dev/cliSignInCode.ts → MCP `MCP_SERVICE_SHARED_SECRET`): a
30
+ * capability-gated internal door, not an ambient env-mode branch.
31
+ */
32
+ import type { APIContext } from "astro";
33
+ import { getSessionStore, setSessionCookie } from "@/lib/auth/route";
34
+ import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
35
+ import { readEnv, readKv } from "@/lib/env";
36
+ import { mintActivityIngestToken } from "@/lib/dev/activityIngestToken";
37
+ import { resolvePreviewTenant } from "@/config/previewTenantLookup";
38
+
39
+ export const prerender = false;
40
+
41
+ function json(body: unknown, status = 200): Response {
42
+ return new Response(JSON.stringify(body), {
43
+ status,
44
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
45
+ });
46
+ }
47
+
48
+ /** Length-independent constant-time string compare (avoids leaking the secret by timing). */
49
+ function timingSafeEqual(a: string, b: string): boolean {
50
+ // XOR the SHA-equalised inputs isn't necessary here; compare fixed-work over the
51
+ // longer length so mismatched lengths still take constant time relative to input.
52
+ const len = Math.max(a.length, b.length);
53
+ let diff = a.length ^ b.length;
54
+ for (let i = 0; i < len; i++) {
55
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
56
+ }
57
+ return diff === 0;
58
+ }
59
+
60
+ export async function POST(context: APIContext): Promise<Response> {
61
+ // GATE 1 — secret must be provisioned; absent ⇒ pretend the route doesn't exist.
62
+ const secret = await readEnv("E2E_TEST_SECRET");
63
+ if (!secret) return json({ error: "not found" }, 404);
64
+
65
+ // GATE 2 — never in the production tier, even if a secret leaked into it.
66
+ const nodeEnv = ((await readEnv("NODE_ENV")) ?? "").toLowerCase();
67
+ if (nodeEnv === "production") return json({ error: "not found" }, 404);
68
+
69
+ // GATE 3 — present the secret.
70
+ const presented = context.request.headers.get("x-e2e-secret") ?? "";
71
+ if (!timingSafeEqual(presented, secret)) return json({ error: "forbidden" }, 403);
72
+
73
+ let body: { email?: unknown; tenant?: unknown };
74
+ try {
75
+ body = (await context.request.json()) as typeof body;
76
+ } catch {
77
+ return json({ error: "invalid JSON body" }, 400);
78
+ }
79
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
80
+ const tenantId = typeof body.tenant === "string" ? body.tenant.trim().toLowerCase() : "";
81
+ if (!email || !tenantId) return json({ error: "email and tenant are required" }, 400);
82
+
83
+ // Bound to a KNOWN store — never mints against an arbitrary/unknown tenant.
84
+ const tenant = await resolvePreviewTenant(tenantId);
85
+ if (!tenant) return json({ error: "unknown tenant" }, 404);
86
+
87
+ const store = await getSessionStore();
88
+ if (!store) return json({ error: "session store unavailable" }, 503);
89
+
90
+ // Mint a store-LINKED developer session (tenants[0].resource is what the
91
+ // /api/dev/activity poll resolves — see api/dev/activity.ts GET).
92
+ const now = Math.floor(Date.now() / 1000);
93
+ const resource = tenant.appDomain;
94
+ const { id } = await createSession(
95
+ store,
96
+ {
97
+ email,
98
+ resource,
99
+ roles: ["developer"],
100
+ capability: "developer",
101
+ tenants: [{ resource, role: "developer" }],
102
+ staff: [],
103
+ },
104
+ DEFAULT_SESSION_TTL_SECONDS,
105
+ now,
106
+ );
107
+ setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
108
+
109
+ // Also hand back an activity-ingest token so the test can POST heartbeat / save /
110
+ // obstacle events the way the local `tot dev` loop would (bearer-authed, not cookie).
111
+ const kv = await readKv("SESSION");
112
+ let activityToken: string | null = null;
113
+ let activityExpiresAt: string | null = null;
114
+ if (kv) {
115
+ const minted = await mintActivityIngestToken(kv, {
116
+ email,
117
+ tenant: tenant.tenant_id,
118
+ capability: "developer",
119
+ });
120
+ activityToken = minted.token;
121
+ activityExpiresAt = minted.expiresAt;
122
+ }
123
+
124
+ return json({
125
+ ok: true,
126
+ email,
127
+ tenant: tenant.tenant_id,
128
+ resource,
129
+ activityToken,
130
+ activityExpiresAt,
131
+ activityUrl: new URL(context.request.url).origin,
132
+ });
133
+ }