@tokenoftrust/storefront-runner 2.4.62 → 2.4.64

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.
@@ -87,10 +87,30 @@ export async function readCartSecret(
87
87
  * but the secret is missing, so NO cart may be built. The caller must render
88
88
  * no buy form (never a tamperable / broken one).
89
89
  */
90
+ /** Shopper copy when checkout cannot be signed. No operator setup detail. */
91
+ export const CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE = "Checkout is unavailable.";
92
+
93
+ /** Product-page alert. The cart signing env var name stays in the server log. */
94
+ export function shopperCheckoutUnavailableHtml(): string {
95
+ return (
96
+ `<p class="mb-3 text-sm text-[var(--color-text)]" role="alert" data-cart-operator-error>` +
97
+ `${CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE}</p>`
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Message safe to put in an HTTP body. `signing-unavailable` carries operator
103
+ * setup text from the cart runtime; shoppers get the neutral message instead.
104
+ */
105
+ export function shopperCartDenialMessage(code: string, message: string): string {
106
+ if (code === "signing-unavailable") return CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE;
107
+ return message;
108
+ }
109
+
90
110
  export interface CartSigningResolution {
91
111
  enabled: boolean;
92
112
  secret?: string;
93
- /** Set when signed mode has no secret. Actionable for the operator. No secret value. */
113
+ /** Operator setup detail. Do not write this into shopper HTML or an HTTP body. */
94
114
  operatorError?: string;
95
115
  }
96
116
 
@@ -26,7 +26,7 @@ import {
26
26
  type CartDenialCode,
27
27
  } from "@tot/public-runtime";
28
28
  import { resolveCheckoutHandoffCorrelation } from "@/lib/commerce/checkoutHandoffCorrelation";
29
- import { resolveCartSigning } from "@/lib/checkoutCommerce";
29
+ import { resolveCartSigning, shopperCartDenialMessage } from "@/lib/checkoutCommerce";
30
30
 
31
31
  export const prerender = false;
32
32
 
@@ -88,7 +88,11 @@ async function cartFormPost(context: APIContext): Promise<Response> {
88
88
  }
89
89
  const signing = await resolveCartSigning(commerce);
90
90
  if (!signing.enabled) {
91
- return cartDenial("signing-unavailable", signing.operatorError ?? "Signed checkout is unavailable.", 503);
91
+ return cartDenial(
92
+ "signing-unavailable",
93
+ shopperCartDenialMessage("signing-unavailable", signing.operatorError ?? ""),
94
+ 503,
95
+ );
92
96
  }
93
97
  const form = await context.request.formData();
94
98
  const entries: Array<[string, string]> = [];
@@ -103,7 +107,7 @@ async function cartFormPost(context: APIContext): Promise<Response> {
103
107
  });
104
108
  if (!result.ok) {
105
109
  const status = result.code === "signing-unavailable" ? 503 : 422;
106
- return cartDenial(result.code, result.message, status);
110
+ return cartDenial(result.code, shopperCartDenialMessage(result.code, result.message), status);
107
111
  }
108
112
  return new Response(null, {
109
113
  status: 303,
@@ -37,7 +37,7 @@ import type {
37
37
  MembershipStackingPolicy,
38
38
  PurchaseOptionPricingContext,
39
39
  } from "@tot/public-runtime";
40
- import { resolveCartSigning } from "@/lib/checkoutCommerce";
40
+ import { resolveCartSigning, shopperCheckoutUnavailableHtml } from "@/lib/checkoutCommerce";
41
41
  import { isRegulatedTenant } from "@/lib/compliance/enforcement";
42
42
  import { resolveMemberContext } from "@/lib/membership/eligibility";
43
43
  import { truncate } from "@/lib/format";
@@ -223,14 +223,14 @@ let checkoutCart:
223
223
  byVariantId: Record<string, Record<string, VariantCartPayload>>;
224
224
  }
225
225
  | undefined;
226
- let cartOperatorError: string | undefined;
226
+ let checkoutUnavailable = false;
227
227
  if (commerce) {
228
228
  // Fail closed for production signed checkout: a signed tenant with no
229
229
  // cart-validation secret builds NO cart (never a tamperable form). See
230
230
  // resolveCartSigning — it also logs the misconfiguration loudly.
231
231
  const signing = await resolveCartSigning(commerce);
232
232
  if (!signing.enabled) {
233
- cartOperatorError = signing.operatorError;
233
+ checkoutUnavailable = true;
234
234
  } else {
235
235
  try {
236
236
  const purchaseOptions = visiblePurchaseOptions(
@@ -265,10 +265,9 @@ if (commerce) {
265
265
  byVariantId: Object.fromEntries(entries),
266
266
  };
267
267
  } catch (err) {
268
- cartOperatorError =
269
- err instanceof Error && err.name === "CartSigningUnavailableError"
270
- ? err.message
271
- : undefined;
268
+ if (err instanceof Error && err.name === "CartSigningUnavailableError") {
269
+ checkoutUnavailable = true;
270
+ }
272
271
  console.warn(
273
272
  `[storefront] Invalid checkout purchase options for ${product.handle}; cart disabled for this product.`,
274
273
  );
@@ -386,11 +385,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
386
385
  The form action is the worker cart route, so a native submit reaches
387
386
  the server check. A clear browser preflight is not payment authority. */}
388
387
  <div class="mt-7" data-payment-authority="none">
389
- {cartOperatorError && (
390
- <p class="mb-3 text-sm text-[var(--color-text)]" role="alert" data-cart-operator-error>
391
- {cartOperatorError}
392
- </p>
393
- )}
388
+ {checkoutUnavailable && <Fragment set:html={shopperCheckoutUnavailableHtml()} />}
394
389
  <VariantSelector
395
390
  client:load
396
391
  variants={publicSelectorVariants(product.variants)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.62",
3
+ "version": "2.4.64",
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",
@@ -424,6 +424,18 @@ async function hmacHex(secret: string, message: string): Promise<string> {
424
424
  .join("");
425
425
  }
426
426
 
427
+ function constantTimeHexEqual(expected: string, presented: string): boolean {
428
+ const enc = new TextEncoder();
429
+ const left = enc.encode(expected);
430
+ const right = enc.encode(presented);
431
+ const length = Math.max(left.length, right.length);
432
+ let acc = left.length ^ right.length;
433
+ for (let i = 0; i < length; i++) {
434
+ acc |= (left[i] ?? 0) ^ (right[i] ?? 0);
435
+ }
436
+ return acc === 0;
437
+ }
438
+
427
439
  /**
428
440
  * Sign one field for product `code`. Editable fields hash the sentinel
429
441
  * `--OPEN--` and gain the `||open` suffix so checkout accepts a user-entered value.
@@ -596,7 +608,7 @@ export async function enforceCartSubmission(input: {
596
608
  if (!field.hash) return false;
597
609
  const valueToHash = field.open ? "--OPEN--" : field.value;
598
610
  const expected = await hmacHex(secret, codeField.value + field.baseName + valueToHash);
599
- return expected === field.hash;
611
+ return constantTimeHexEqual(expected, field.hash);
600
612
  };
601
613
 
602
614
  if (signed) {