@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.2

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 (79) hide show
  1. package/apps/storefront/astro.config.mjs +59 -0
  2. package/apps/storefront/env.d.ts +24 -8
  3. package/apps/storefront/package.json +1 -1
  4. package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
  5. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  6. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
  7. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
  8. package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
  9. package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
  10. package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
  11. package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
  12. package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
  13. package/apps/storefront/src/config/devTenantSeed.ts +65 -16
  14. package/apps/storefront/src/config/resolver.ts +48 -1
  15. package/apps/storefront/src/layouts/Layout.astro +14 -12
  16. package/apps/storefront/src/lib/admin/README.md +2 -2
  17. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
  18. package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
  19. package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
  20. package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
  21. package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
  22. package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
  23. package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
  24. package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
  25. package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
  26. package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
  27. package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
  28. package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
  29. package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
  30. package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
  31. package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
  32. package/apps/storefront/src/lib/edgeCache.ts +7 -6
  33. package/apps/storefront/src/lib/health.ts +53 -0
  34. package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
  35. package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
  36. package/apps/storefront/src/lib/sections.ts +19 -23
  37. package/apps/storefront/src/lib/securityHeaders.ts +1 -1
  38. package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
  39. package/apps/storefront/src/middleware/index.ts +29 -12
  40. package/apps/storefront/src/pages/[...slug].astro +5 -6
  41. package/apps/storefront/src/pages/admin.astro +10 -3
  42. package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
  43. package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
  44. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
  45. package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
  46. package/apps/storefront/src/pages/auth/login.astro +4 -1
  47. package/apps/storefront/src/pages/auth/magic.astro +263 -43
  48. package/apps/storefront/src/pages/capabilities.astro +1 -1
  49. package/apps/storefront/src/pages/cockpit.astro +2365 -0
  50. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
  51. package/apps/storefront/src/pages/index.astro +23 -29
  52. package/apps/storefront/src/pages/products/[handle].astro +50 -20
  53. package/package.json +6 -2
  54. package/packages/public-runtime/src/capabilities.ts +97 -9
  55. package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
  56. package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
  57. package/packages/public-runtime/src/checkout-style.ts +13 -13
  58. package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
  59. package/packages/public-runtime/src/csp.ts +11 -11
  60. package/packages/public-runtime/src/customization-scripts.ts +28 -0
  61. package/packages/public-runtime/src/extension-contract.ts +73 -0
  62. package/packages/public-runtime/src/index.ts +4 -2
  63. package/packages/public-runtime/src/product.ts +57 -0
  64. package/packages/public-runtime/src/raw-html-policy.ts +35 -0
  65. package/packages/public-runtime/src/tenant.ts +14 -11
  66. package/pnpm-workspace.yaml +1 -0
  67. package/scripts/dev/checkout-watch.mjs +9 -0
  68. package/scripts/dev/file-browser.mjs +5 -1
  69. package/scripts/dev/transient-files.mjs +34 -0
  70. package/scripts/tot-dev.mjs +16 -3
  71. package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
  72. package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
  73. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
  74. package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
  75. package/apps/storefront/src/pages/api/dev/file.ts +0 -57
  76. package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
  77. package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
  78. package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
  79. package/apps/storefront/src/pages/api/dev/write.ts +0 -116
@@ -29,7 +29,7 @@ import {
29
29
  resolveCustomizationEnv,
30
30
  buildCsp,
31
31
  STOREFRONT_CSP_ALLOWLIST,
32
- foxyCspAdditions,
32
+ checkoutCspAdditions,
33
33
  CSP_REPORT_GROUP,
34
34
  CSP_REPORT_PATH,
35
35
  readPublishedArtifact,
@@ -39,7 +39,7 @@ import {
39
39
  capabilitiesArtifactPath,
40
40
  } from "@tot/public-runtime";
41
41
  import type { CapabilityConfig } from "@tot/public-runtime";
42
- import { resolveTenantCommerce } from "@/lib/foxyCommerce";
42
+ import { resolveTenantCommerce } from "@/lib/checkoutCommerce";
43
43
  import { evaluateDemoGate, hostInGateList } from "@/lib/demoGate";
44
44
  import {
45
45
  evaluateLoginGate,
@@ -51,6 +51,7 @@ import {
51
51
  import { gatedHoldingResponse } from "@/lib/auth/gatePage";
52
52
  import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
53
53
  import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
54
+ import { HEALTH_PATH, healthResponse } from "@/lib/health";
54
55
  import { membershipFor } from "@/lib/dashboard/tenantSelection";
55
56
  import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
56
57
 
@@ -60,6 +61,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
60
61
  const host =
61
62
  request.headers.get("host") ?? url.host ?? "localhost";
62
63
 
64
+ // ---- /health — liveness + deploy-identity probe ---------------------------
65
+ // Answered FIRST: before tenant resolution, the access gate, the maintenance
66
+ // kill-switch, and the edge cache — so it reports the running build's commit SHA
67
+ // regardless of Host, an unresolved tenant, or a closed site. See lib/health.
68
+ if (url.pathname === HEALTH_PATH) {
69
+ return healthResponse();
70
+ }
71
+
63
72
  // ---- Site maintenance kill-switch (fail-safe full close) ------------------
64
73
  // MAINTENANCE_MODE=1 takes the ENTIRE site offline (every host + path) with a
65
74
  // static "Coming Soon" holding page, BEFORE tenant resolution, the access
@@ -324,7 +333,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
324
333
  // read these instead of hard-coding tenant identity checks.
325
334
  locals.features = resolveTenantFeatures(tenant);
326
335
  locals.capabilities = capabilities;
327
- // FoxyCart cart/checkout facts (Track A) — only when the tenant has a catalog
336
+ // Cart/checkout facts (Track A) — only when the tenant has a catalog
328
337
  // AND the cartCheckout capability is enabled for THIS environment. Drives the
329
338
  // loader + signed add-to-cart, and the CSP additions below.
330
339
  const commerce =
@@ -356,6 +365,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
356
365
  // the tenant prefix on the client.
357
366
  locals.basePath = basePath;
358
367
 
368
+ // /dev was renamed to /cockpit (2026-07-20). 302-redirect any old /dev link —
369
+ // host-routed (/dev) or path-routed (/<vendor>/dev) — to the new path, preserving
370
+ // the tenant prefix + query. Temporary (302) while the cockpit UX is still evolving.
371
+ if (renderPath === "/dev" || renderPath.startsWith("/dev/")) {
372
+ const rest = renderPath.slice("/dev".length); // "" or "/sub…"
373
+ return Response.redirect(`${url.origin}${basePath}/cockpit${rest}${url.search}`, 302);
374
+ }
375
+
359
376
  // When a /<domain>/ prefix selected the tenant, rewrite to the stripped route
360
377
  // so the existing file-based routes match. next(path) rewrites WITHOUT
361
378
  // re-running middleware, so the locals set above are preserved.
@@ -406,10 +423,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
406
423
  "Reporting-Endpoints",
407
424
  `${CSP_REPORT_GROUP}="${CSP_REPORT_PATH}"`,
408
425
  );
409
- // FoxyCart origins (loader CDN + the store's cart/checkout host) added to the
426
+ // Checkout origins (loader CDN + the store's cart/checkout host) added to the
410
427
  // strict policy only when this tenant has a store — so add-to-cart, the
411
428
  // sidecart iframe/XHR, and the cross-origin form POST all pass.
412
- const foxyCsp = commerce ? foxyCspAdditions(commerce.origin) : null;
429
+ const checkoutCsp = commerce ? checkoutCspAdditions(commerce.origin) : null;
413
430
  response.headers.set(
414
431
  cspHeader,
415
432
  buildCsp({
@@ -420,21 +437,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
420
437
  scriptSrc: [
421
438
  ...STOREFRONT_CSP_ALLOWLIST.scriptSrc,
422
439
  ...STATIC_SCRIPT_HASHES,
423
- ...(foxyCsp?.scriptSrc ?? []),
440
+ ...(checkoutCsp?.scriptSrc ?? []),
424
441
  ],
425
442
  styleSrc: [
426
443
  ...STOREFRONT_CSP_ALLOWLIST.styleSrc,
427
444
  ...STATIC_STYLE_HASHES,
428
- ...(foxyCsp?.styleSrc ?? []),
445
+ ...(checkoutCsp?.styleSrc ?? []),
429
446
  ],
430
- fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(foxyCsp?.fontSrc ?? [])],
447
+ fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(checkoutCsp?.fontSrc ?? [])],
431
448
  connectSrc: [
432
449
  ...STOREFRONT_CSP_ALLOWLIST.connectSrc,
433
- ...(foxyCsp?.connectSrc ?? []),
450
+ ...(checkoutCsp?.connectSrc ?? []),
434
451
  ],
435
- imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(foxyCsp?.imgSrc ?? [])],
436
- frameSrc: foxyCsp?.frameSrc ?? [],
437
- formAction: foxyCsp?.formAction ?? [],
452
+ imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(checkoutCsp?.imgSrc ?? [])],
453
+ frameSrc: checkoutCsp?.frameSrc ?? [],
454
+ formAction: checkoutCsp?.formAction ?? [],
438
455
  reportGroup: CSP_REPORT_GROUP,
439
456
  reportUri: CSP_REPORT_PATH,
440
457
  }),
@@ -9,7 +9,7 @@ import Layout from "@/layouts/Layout.astro";
9
9
  import Breadcrumbs from "@/components/Breadcrumbs.astro";
10
10
  import { breadcrumbLd } from "@/lib/jsonld";
11
11
  import { truncate } from "@/lib/format";
12
- import { rawHtmlResponse } from "@/lib/rawMarketingHtml";
12
+ import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
13
13
 
14
14
  const { tenant, canonicalBase, content, basePath } = Astro.locals;
15
15
  const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
@@ -34,12 +34,11 @@ if (slug === "_style-guide" || slug.startsWith("_style-guide/")) {
34
34
  }
35
35
 
36
36
  // Raw tenant-authored marketing subpage (vendor-injected HTML) — serve the
37
- // self-contained document VERBATIM, bypassing the block Layout, with the request
38
- // CSP nonce stamped onto its first-party inline <style>/<script>. Same path as the
39
- // raw home in index.astro. Middleware still applies canonical host, security
40
- // headers, CSP, and (prod) the edge cache.
37
+ // self-contained document VERBATIM only when rawHtmlPolicy says this tenant may
38
+ // bypass Layout. Regulated commerce tenants fall through to platform-composed
39
+ // pages so compliance UI cannot be removed by raw HTML.
41
40
  const rawHtml = await content.getPageHtml(slug);
42
- if (rawHtml) {
41
+ if (rawHtml && rawHtmlPolicy(tenant).allowed) {
43
42
  // Shared chrome: a body-only fragment gets wrapped in the tenant's chrome.html;
44
43
  // a full document serves verbatim (backward-compatible). See rawMarketingHtml.ts.
45
44
  const chromeWrapper = await content.getChromeHtml();
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * SHELL CONTRACT — every AdminXTab.astro component mounted below must preserve:
14
14
  * - A `<section id="{tab}" data-admin-panel data-agent-scope="{tab}">` root,
15
- * where {tab} is one of: orders, products, customers, fulfillment,
15
+ * where {tab} is one of: orders, products, subscriptions, customers, fulfillment,
16
16
  * reporting, ai-workflows, settings. The shell's hash router shows/hides
17
17
  * panels by matching `html[data-admin-tab]` to this id, and the AI Ask
18
18
  * envelope reads `data-agent-scope` to label its active context.
@@ -35,6 +35,7 @@ import AdminFulfillmentTab from "../components/admin/AdminFulfillmentTab.astro";
35
35
  import AdminAIWorkflowsTab from "../components/admin/AdminAIWorkflowsTab.astro";
36
36
  import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
37
37
  import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
38
+ import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
38
39
  import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
39
40
  import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
40
41
  import { readViewerSession } from "@/lib/auth/route";
@@ -79,7 +80,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
79
80
  <title>{tenantLabel} Admin</title>
80
81
  <script is:inline nonce={cspNonce}>
81
82
  (() => {
82
- const adminTabs = ["orders", "products", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
83
+ const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
83
84
  const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
84
85
  const defaultTab = canViewInternalGuide ? "orders" : "settings";
85
86
  const hash = window.location.hash.slice(1);
@@ -502,6 +503,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
502
503
 
503
504
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="orders"] #orders,
504
505
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="products"] #products,
506
+ html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="subscriptions"] #subscriptions,
505
507
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="customers"] #customers,
506
508
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
507
509
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
@@ -1126,6 +1128,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1126
1128
  <nav class="nav">
1127
1129
  <a href="#orders" data-admin-tab-link><svg><use href="#i-file"></use></svg><span>Orders</span></a>
1128
1130
  <a href="#products" data-admin-tab-link><svg><use href="#i-box"></use></svg><span>Products</span></a>
1131
+ <a href="#subscriptions" data-admin-tab-link><svg><use href="#i-refresh"></use></svg><span>Subscriptions</span></a>
1129
1132
  <a href="#customers" data-admin-tab-link><svg><use href="#i-users"></use></svg><span>Customers</span></a>
1130
1133
  <a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
1131
1134
  <a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
@@ -1151,6 +1154,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1151
1154
  <div class="tabs mobile-tabbar" role="tablist" aria-label="Admin work areas">
1152
1155
  <a class="tab" href="#orders" role="tab" aria-controls="orders" aria-selected="false" data-admin-tab-link>Orders</a>
1153
1156
  <a class="tab" href="#products" role="tab" aria-controls="products" aria-selected="false" data-admin-tab-link>Products</a>
1157
+ <a class="tab" href="#subscriptions" role="tab" aria-controls="subscriptions" aria-selected="false" data-admin-tab-link>Subscriptions</a>
1154
1158
  <a class="tab" href="#customers" role="tab" aria-controls="customers" aria-selected="false" data-admin-tab-link>Customers</a>
1155
1159
  <a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1156
1160
  <a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
@@ -1197,6 +1201,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1197
1201
  <div class="tabs admin-tabs" role="tablist" aria-label="Admin work areas">
1198
1202
  <a id="admin-tab-orders" class="tab active" href="#orders" role="tab" aria-controls="orders" aria-selected="true" data-admin-tab-link>Orders</a>
1199
1203
  <a id="admin-tab-products" class="tab" href="#products" role="tab" aria-controls="products" aria-selected="false" data-admin-tab-link>Products</a>
1204
+ <a id="admin-tab-subscriptions" class="tab" href="#subscriptions" role="tab" aria-controls="subscriptions" aria-selected="false" data-admin-tab-link>Subscriptions</a>
1200
1205
  <a id="admin-tab-customers" class="tab" href="#customers" role="tab" aria-controls="customers" aria-selected="false" data-admin-tab-link>Customers</a>
1201
1206
  <a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1202
1207
  <a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
@@ -1321,6 +1326,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1321
1326
 
1322
1327
  <OrdersWorkspace envelope={tenantEnvelope} />
1323
1328
  <AdminProductsTab envelope={tenantEnvelope} />
1329
+ <AdminSubscriptionsTab envelope={tenantEnvelope} />
1324
1330
  <AdminCustomersTab envelope={tenantEnvelope} />
1325
1331
  <AdminFulfillmentTab envelope={tenantEnvelope} />
1326
1332
  <AdminReportingTab envelope={tenantEnvelope} />
@@ -1441,7 +1447,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1441
1447
 
1442
1448
  <script is:inline nonce={cspNonce}>
1443
1449
  (() => {
1444
- const tabIds = ["orders", "products", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
1450
+ const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
1445
1451
  const tabSet = new Set(tabIds);
1446
1452
  const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
1447
1453
  const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
@@ -1449,6 +1455,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1449
1455
  const askContexts = {
1450
1456
  orders: "Orders",
1451
1457
  products: "Products",
1458
+ subscriptions: "Subscriptions",
1452
1459
  customers: "Customers",
1453
1460
  fulfillment: "Fulfillment",
1454
1461
  reporting: "Reporting",
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * `GET /api/admin/orders/:id` — one order's full detail for the Orders tab (U2).
3
3
  *
4
- * Server-side proxy to tot-foxycart `GET /commerce/orders/:orderId` for the
5
- * resolved tenant + env. Bearer stays server-side; response is the FoxyOrderDetail
4
+ * Server-side proxy to checkout `GET /commerce/orders/:orderId` for the
5
+ * resolved tenant + env. Bearer stays server-side; response is the CheckoutOrderDetail
6
6
  * DTO. `verificationDetail` is empty until U4 fills it — consumers tolerate that.
7
7
  * Read-only (scope-locks).
8
8
  */
9
9
  import type { APIContext } from "astro";
10
10
  import { readEnv } from "@/lib/env";
11
11
  import {
12
- createFoxyOrderClient,
12
+ createCheckoutOrderClient,
13
13
  orderApiBaseUrl,
14
14
  OrderApiError,
15
15
  } from "@/lib/admin/orderClient";
@@ -37,7 +37,7 @@ export async function GET(context: APIContext): Promise<Response> {
37
37
  const bearer = await readEnv("FOXY_ORDER_API_KEY");
38
38
  if (!bearer) return jsonError("not_provisioned", 503);
39
39
 
40
- const client = createFoxyOrderClient({
40
+ const client = createCheckoutOrderClient({
41
41
  baseUrl: orderApiBaseUrl(customizationEnv),
42
42
  appDomain: tenant.appDomain,
43
43
  bearer,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `GET /api/admin/orders` — internal admin route backing the Orders tab (U2).
3
3
  *
4
- * Server-side proxy to tot-foxycart's order READ API for the resolved tenant +
4
+ * Server-side proxy to the checkout order READ API for the resolved tenant +
5
5
  * environment. The per-tenant bearer key stays on the Worker (never in the
6
6
  * response); the browser only ever sees order DTO JSON. Read-only (scope-locks).
7
7
  * U3 consumes this and maps the DTO → OrdersViewModel.
@@ -10,10 +10,10 @@
10
10
  * host/approver layer) — same posture as `api/evidence.json.ts`; no new gate here.
11
11
  */
12
12
  import type { APIContext } from "astro";
13
- import type { FoxyOrderListFilters } from "@tot/public-runtime";
13
+ import type { CheckoutOrderListFilters } from "@tot/public-runtime";
14
14
  import { readEnv } from "@/lib/env";
15
15
  import {
16
- createFoxyOrderClient,
16
+ createCheckoutOrderClient,
17
17
  orderApiBaseUrl,
18
18
  OrderApiError,
19
19
  } from "@/lib/admin/orderClient";
@@ -46,7 +46,7 @@ export async function GET(context: APIContext): Promise<Response> {
46
46
  const url = new URL(context.request.url);
47
47
  const q = url.searchParams;
48
48
  const limitRaw = q.get("limit");
49
- const filters: FoxyOrderListFilters = {
49
+ const filters: CheckoutOrderListFilters = {
50
50
  queue: q.get("queue") ?? undefined,
51
51
  status: q.get("status") ?? undefined,
52
52
  from: q.get("from") ?? undefined,
@@ -55,7 +55,7 @@ export async function GET(context: APIContext): Promise<Response> {
55
55
  cursor: q.get("cursor") ?? undefined,
56
56
  };
57
57
 
58
- const client = createFoxyOrderClient({
58
+ const client = createCheckoutOrderClient({
59
59
  baseUrl: orderApiBaseUrl(customizationEnv),
60
60
  appDomain: tenant.appDomain,
61
61
  bearer,
@@ -24,16 +24,19 @@ import {
24
24
  sessionFromFederatedClaims,
25
25
  } from "@/lib/auth/identityToken";
26
26
  import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
27
+ import { normalizeEmailHint } from "@/lib/privacy/emailHint";
27
28
 
28
29
  export const prerender = false;
29
30
 
30
31
  /** Back to the landing page carrying an error (no token in the URL on failure). */
31
32
  function magicRedirect(
32
33
  context: APIContext,
33
- params: { error?: string; next?: string },
34
+ params: { error?: string; next?: string; tenant?: string; emailHint?: string },
34
35
  ): Response {
35
36
  const q = new URLSearchParams();
36
37
  if (params.error) q.set("error", params.error);
38
+ if (params.tenant) q.set("tenant", params.tenant);
39
+ if (params.emailHint) q.set("email_hint", params.emailHint);
37
40
  if (params.next && params.next !== "/dashboard") q.set("next", params.next);
38
41
  const qs = q.toString();
39
42
  return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
@@ -42,19 +45,23 @@ function magicRedirect(
42
45
  export async function POST(context: APIContext): Promise<Response> {
43
46
  let token = "";
44
47
  let next = "/dashboard";
48
+ let tenant = "";
49
+ let emailHint = "";
45
50
  try {
46
51
  const form = await context.request.formData();
47
52
  token = String(form.get("token") ?? "").trim();
53
+ tenant = String(form.get("tenant") ?? "").trim().toLowerCase();
54
+ emailHint = normalizeEmailHint(String(form.get("email_hint") ?? "")) ?? "";
48
55
  next = safeNext(String(form.get("next") ?? "/dashboard"));
49
56
  } catch {
50
57
  return magicRedirect(context, { error: "invalid", next });
51
58
  }
52
- if (!token) return magicRedirect(context, { error: "missing", next });
59
+ if (!token) return magicRedirect(context, { error: "missing", tenant, emailHint, next });
53
60
 
54
61
  const client = await getAccessClient();
55
62
  const store = await getSessionStore();
56
63
  if (!client || !store) {
57
- return magicRedirect(context, { error: "unconfigured", next });
64
+ return magicRedirect(context, { error: "unconfigured", tenant, emailHint, next });
58
65
  }
59
66
 
60
67
  // Consume the token (single-use; burned server-side by tot20).
@@ -62,19 +69,19 @@ export async function POST(context: APIContext): Promise<Response> {
62
69
  if (!result.ok) {
63
70
  // reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
64
71
  const error = result.reason === "expired" ? "expired" : "invalid";
65
- return magicRedirect(context, { error, next });
72
+ return magicRedirect(context, { error, tenant, emailHint, next });
66
73
  }
67
74
 
68
75
  // Prove authorization offline — fail closed with no flat-JSON fallback.
69
76
  const idToken = result.id_token;
70
77
  if (!idToken) {
71
78
  console.error("auth/magic-exchange: no id_token on exchange; failing closed");
72
- return magicRedirect(context, { error: "verify_failed", next });
79
+ return magicRedirect(context, { error: "verify_failed", tenant, emailHint, next });
73
80
  }
74
81
  const verifier = await getFederatedVerifier();
75
82
  if (!verifier) {
76
83
  console.error("auth/magic-exchange: federated verifier unconfigured");
77
- return magicRedirect(context, { error: "unconfigured", next });
84
+ return magicRedirect(context, { error: "unconfigured", tenant, emailHint, next });
78
85
  }
79
86
 
80
87
  let derived;
@@ -85,7 +92,7 @@ export async function POST(context: APIContext): Promise<Response> {
85
92
  console.error("auth/magic-exchange: id_token verification failed; failing closed", {
86
93
  error: err instanceof Error ? err.message : String(err),
87
94
  });
88
- return magicRedirect(context, { error: "verify_failed", next });
95
+ return magicRedirect(context, { error: "verify_failed", tenant, emailHint, next });
89
96
  }
90
97
 
91
98
  const nowSeconds = Math.floor(Date.now() / 1000);
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * `GET /api/checkout-style.css` — the stable, per-tenant storefront URL that hosts
3
3
  * the published checkout styling as `text/css`. This URL is what U11's publisher
4
- * points FoxyCart's checkout-template `content_url` at (PUT /theme/foxy) and what
5
- * Foxy fetches server-side when it (re)caches the checkout template.
4
+ * points hosted checkout's `content_url` at and what checkout fetches server-side
5
+ * when it (re)caches the checkout template.
6
6
  *
7
- * The tenant + environment are self-identifying from the request Host (Foxy's
7
+ * The tenant + environment are self-identifying from the request Host (checkout's
8
8
  * fetch carries no session): the middleware resolves `locals.tenant` +
9
9
  * `locals.customizationEnv`, and we serve exactly that tenant's published style
10
10
  * for that env (live on prod hosts, test behind a preview token). Falls back to
11
11
  * the neutral DEFAULT_CHECKOUT_STYLE tokens when nothing is published, so the URL
12
12
  * is always a valid, non-empty stylesheet.
13
13
  *
14
- * ASCII-safety (Foxy cache is not Unicode-safe) is enforced inside
14
+ * ASCII-safety (checkout cache is not Unicode-safe) is enforced inside
15
15
  * buildCheckoutStyleCss. No secrets, no cross-service call — read-only from KV.
16
16
  */
17
17
  import type { APIContext } from "astro";
@@ -34,7 +34,7 @@ export async function GET(context: APIContext): Promise<Response> {
34
34
  return new Response(css, {
35
35
  headers: {
36
36
  "content-type": "text/css; charset=utf-8",
37
- // Foxy caches on its side via /theme/foxy/refresh; keep the origin fresh so
37
+ // Checkout caches on its side via the theme refresh; keep the origin fresh so
38
38
  // a re-cache always pulls the latest published styling.
39
39
  "cache-control": "no-store",
40
40
  "x-robots-tag": "noindex, nofollow",
@@ -10,12 +10,15 @@
10
10
  * (tot20 sends/verifies the code, the storefront mints the session). No secrets
11
11
  * reach the browser. Inline style carries the per-request CSP nonce.
12
12
  */
13
+ import { normalizeEmailHint } from "@/lib/privacy/emailHint";
14
+
13
15
  export const prerender = false;
14
16
 
15
17
  const cspNonce = Astro.locals.cspNonce;
16
18
  const url = new URL(Astro.request.url);
17
19
  const sent = url.searchParams.get("sent") === "1";
18
20
  const email = (url.searchParams.get("email") ?? "").trim();
21
+ const emailHint = normalizeEmailHint(url.searchParams.get("email_hint")) ?? "";
19
22
  const errorCode = url.searchParams.get("error");
20
23
  const next = (() => {
21
24
  const n = url.searchParams.get("next") ?? "/";
@@ -132,7 +135,7 @@ if (devMode) {
132
135
  ) : (
133
136
  <>
134
137
  <h1>Sign in</h1>
135
- <p class="sub"><span class="who">Token of Trust© Regulated Storefront</span> is in early access. Enter your email — if you have access, we'll send a one-time code.</p>
138
+ <p class="sub"><span class="who">Token of Trust© Regulated Storefront</span> is in early access. Enter your email — if you have access, we'll send a one-time code.{emailHint ? <> Use the invite email hinted as <span class="who">{emailHint}</span>.</> : null}</p>
136
139
  <form method="post" action="/api/auth/send">
137
140
  <input type="hidden" name="next" value={next} />
138
141
  <label for="email">Work email</label>