@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
|
-
/**
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
{
|
|
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.
|
|
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
|
|
611
|
+
return constantTimeHexEqual(expected, field.hash);
|
|
600
612
|
};
|
|
601
613
|
|
|
602
614
|
if (signed) {
|