@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.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 (75) hide show
  1. package/apps/storefront/astro.config.mjs +50 -0
  2. package/apps/storefront/env.d.ts +24 -8
  3. package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
  4. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  5. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
  6. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
  7. package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
  8. package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
  9. package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
  10. package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
  11. package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
  12. package/apps/storefront/src/config/devTenantSeed.ts +65 -16
  13. package/apps/storefront/src/config/resolver.ts +48 -1
  14. package/apps/storefront/src/layouts/Layout.astro +14 -12
  15. package/apps/storefront/src/lib/admin/README.md +2 -2
  16. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
  17. package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
  18. package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
  19. package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
  20. package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
  21. package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
  22. package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
  23. package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
  24. package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
  25. package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
  26. package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
  27. package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
  28. package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
  29. package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
  30. package/apps/storefront/src/lib/dev/runtimeStore.ts +136 -7
  31. package/apps/storefront/src/lib/edgeCache.ts +7 -6
  32. package/apps/storefront/src/lib/health.ts +53 -0
  33. package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
  34. package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
  35. package/apps/storefront/src/lib/sections.ts +19 -23
  36. package/apps/storefront/src/lib/securityHeaders.ts +1 -1
  37. package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
  38. package/apps/storefront/src/middleware/index.ts +29 -12
  39. package/apps/storefront/src/pages/[...slug].astro +5 -6
  40. package/apps/storefront/src/pages/admin.astro +10 -3
  41. package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
  42. package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
  43. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
  44. package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
  45. package/apps/storefront/src/pages/auth/login.astro +4 -1
  46. package/apps/storefront/src/pages/auth/magic.astro +263 -43
  47. package/apps/storefront/src/pages/capabilities.astro +1 -1
  48. package/apps/storefront/src/pages/cockpit.astro +2365 -0
  49. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
  50. package/apps/storefront/src/pages/index.astro +23 -29
  51. package/apps/storefront/src/pages/products/[handle].astro +50 -20
  52. package/package.json +1 -1
  53. package/packages/public-runtime/src/capabilities.ts +97 -9
  54. package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
  55. package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
  56. package/packages/public-runtime/src/checkout-style.ts +13 -13
  57. package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
  58. package/packages/public-runtime/src/csp.ts +11 -11
  59. package/packages/public-runtime/src/customization-scripts.ts +28 -0
  60. package/packages/public-runtime/src/extension-contract.ts +73 -0
  61. package/packages/public-runtime/src/index.ts +4 -2
  62. package/packages/public-runtime/src/product.ts +57 -0
  63. package/packages/public-runtime/src/raw-html-policy.ts +35 -0
  64. package/packages/public-runtime/src/tenant.ts +14 -11
  65. package/scripts/dev/checkout-watch.mjs +28 -0
  66. package/scripts/dev/file-browser.mjs +5 -1
  67. package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
  68. package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
  69. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
  70. package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
  71. package/apps/storefront/src/pages/api/dev/file.ts +0 -57
  72. package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
  73. package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
  74. package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
  75. package/apps/storefront/src/pages/api/dev/write.ts +0 -116
@@ -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>