@tokenoftrust/storefront-runner 2.4.81 → 2.4.83

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.
@@ -26,6 +26,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
26
26
  import type { CartComplianceDecision } from "@tot/public-runtime";
27
27
  import {
28
28
  clearStoredRef,
29
+ isStoreNotPublicRefusal,
29
30
  parsePreflightResponse,
30
31
  pollVerification,
31
32
  readStoredRef,
@@ -91,6 +92,10 @@ type DialogPhase = VerificationPhase | "stalled";
91
92
  const FAIL_CLOSED_MESSAGE =
92
93
  "We couldn't reach our compliance check. For your safety, checkout is paused — please try again.";
93
94
 
95
+ /** The store-not-public gate refused the preflight: checkout needs a signed-in session. */
96
+ const STORE_NOT_PUBLIC_MESSAGE =
97
+ "This store isn't open to the public yet, so checkout is unavailable here. Sign in to the store to test checkout.";
98
+
94
99
  export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props) {
95
100
  const [pending, setPending] = useState<PendingLine | null>(null);
96
101
  const [region, setRegion] = useState("");
@@ -110,6 +115,8 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
110
115
  const [popupBlocked, setPopupBlocked] = useState(false);
111
116
  /** Cancellation token for the in-flight poll loop (at most one at a time). */
112
117
  const pollToken = useRef<{ cancelled: boolean } | null>(null);
118
+ /** Set by the last preflight POST when the store-not-public gate answered it. */
119
+ const gateRefused = useRef(false);
113
120
 
114
121
  const stopPolling = useCallback(() => {
115
122
  if (pollToken.current) pollToken.current.cancelled = true;
@@ -191,6 +198,7 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
191
198
  opts: { ref: string | null; includeShopper: boolean },
192
199
  ): Promise<PreflightResponse | null> => {
193
200
  const trimmedEmail = email.trim();
201
+ gateRefused.current = false;
194
202
  try {
195
203
  const res = await fetch(preflightUrl, {
196
204
  method: "POST",
@@ -218,6 +226,10 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
218
226
  : {}),
219
227
  }),
220
228
  });
229
+ if (isStoreNotPublicRefusal(res)) {
230
+ gateRefused.current = true;
231
+ return null;
232
+ }
221
233
  return parsePreflightResponse(await res.json());
222
234
  } catch {
223
235
  return null;
@@ -336,7 +348,7 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
336
348
  setLoading(false);
337
349
  if (!res) {
338
350
  // Fail closed: a regulated tenant never proceeds on an unreadable result.
339
- setError(FAIL_CLOSED_MESSAGE);
351
+ setError(gateRefused.current ? STORE_NOT_PUBLIC_MESSAGE : FAIL_CLOSED_MESSAGE);
340
352
  return;
341
353
  }
342
354
  apply(pending, res);
@@ -173,6 +173,15 @@ export interface PollDeps {
173
173
  timeoutMs?: number;
174
174
  }
175
175
 
176
+ /**
177
+ * True when the login gate, not the preflight route, answered: the store is not
178
+ * public and this viewer holds no session (see `lib/auth/gatePage.ts`). Still fail
179
+ * closed — the caller only names the reason instead of reporting an outage.
180
+ */
181
+ export function isStoreNotPublicRefusal(res: Pick<Response, "status" | "headers">): boolean {
182
+ return res.status === 401 && res.headers.get("x-tot-gate") === "store-not-public";
183
+ }
184
+
176
185
  /**
177
186
  * Re-read the server's decision until it resolves. The client NEVER decides the
178
187
  * shopper passed — it only relays `decision.status === "clear"`.
@@ -5,8 +5,8 @@
5
5
  * delegated over HTTP to tot20, and there is no bulk/marketing sender in-repo
6
6
  * (docs/plans/2026-08-22-newsletter-send-spike.md §1). Wiring the real send is
7
7
  * the ESP-pipeline's job — it replaces the body of this function with a
8
- * transactional send (confirm link in the body). Until then we only LOG intent,
9
- * and never let a send failure fail the subscribe (the pending row + returned
8
+ * transactional send (confirm link in the body). The stub logs only a static event,
9
+ * and never lets a send failure fail the subscribe (the pending row and its
10
10
  * token are the durable state; the mail is a follow-up).
11
11
  */
12
12
 
@@ -22,9 +22,6 @@ export interface ConfirmationEmailRequest {
22
22
  * Best-effort by contract — callers do not await deliverability, and this never
23
23
  * throws.
24
24
  */
25
- export async function sendConfirmationEmail(req: ConfirmationEmailRequest): Promise<void> {
26
- console.log(
27
- `[newsletter] confirmation email STUB — would send to ${req.email} ` +
28
- `(collection: ${req.collection}) confirm: ${req.confirmUrl}`,
29
- );
25
+ export async function sendConfirmationEmail(_req: ConfirmationEmailRequest): Promise<void> {
26
+ console.log("[newsletter] confirmation email delivery is not configured");
30
27
  }
@@ -110,7 +110,7 @@ export const POST: APIRoute = async (context) => {
110
110
  if (!d1) {
111
111
  // No apps DB binding (local/dev without D1): can't persist. Don't fail the
112
112
  // visitor — log and acknowledge so the flow is exercisable in dev.
113
- console.log(`[newsletter] subscribe (no D1) ${email} → ${tenantId}/${collection}`);
113
+ console.log(`[newsletter] subscribe not persisted (no D1) → ${tenantId}/${collection}`);
114
114
  return ok(context.request);
115
115
  }
116
116
 
@@ -133,14 +133,22 @@ export const POST: APIRoute = async (context) => {
133
133
  // or unsubscribed address. Best-effort; a stub failure must not fail signup.
134
134
  if (result.status === "pending") {
135
135
  const origin = new URL(context.request.url).origin;
136
- const basePath = context.locals?.basePath ?? "";
137
- const confirmUrl = `${origin}${basePath}/api/newsletter/confirm?token=${result.token}`;
138
- await sendConfirmationEmail({ email, collection, confirmUrl }).catch(() => {});
136
+ // The tenant's real routed prefix: a pinned preview's basePath has no API mirror.
137
+ const apiBasePath = context.locals?.apiBasePath ?? context.locals?.basePath ?? "";
138
+ const confirmUrl = `${origin}${apiBasePath}/api/newsletter/confirm?token=${result.token}`;
139
+ await sendConfirmationEmail({ email, collection, confirmUrl }).catch((err: unknown) => {
140
+ console.error(
141
+ `[newsletter] confirmation email failed tenant=${tenantId} collection=${collection}: ${err instanceof Error ? err.name : "unknown"}`,
142
+ );
143
+ });
139
144
  }
140
145
 
141
146
  return ok(context.request);
142
- } catch {
143
- // Never 500 on a signup — acknowledge and move on.
147
+ } catch (err) {
148
+ // Never 500 on a signup — acknowledge, but never swallow the failure silently.
149
+ console.error(
150
+ `[newsletter] subscribe failed collection=${collection}: ${err instanceof Error ? err.name : "unknown"}`,
151
+ );
144
152
  return ok(context.request);
145
153
  }
146
154
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.81",
3
+ "version": "2.4.83",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",