@tokenoftrust/storefront-runner 3.8.0 → 3.9.0
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
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",
|
|
@@ -329,16 +329,46 @@ export const VISIBLE_CART_FIELDS = ["Purchase option"] as const;
|
|
|
329
329
|
|
|
330
330
|
export type CartFieldVisibility = "native" | "internal" | "visible" | "variant-option" | "undeclared";
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Characters the checkout store does not keep in a field name, each mapped to a look-alike it
|
|
334
|
+
* keeps. The store validates the name as it rewrites it, not as it was signed: ASCII space, `.`
|
|
335
|
+
* and `[` become `_` (a `[…]` pair turns the field into an array), `||` ends the name, and a name
|
|
336
|
+
* holding `&` or `"` is dropped from validation. Measured 2026-10-23 against tot-preview.foxycart.com
|
|
337
|
+
* by submitting each name with a non-matching hash and reading the name back from its
|
|
338
|
+
* "Cart Validation Error: <name>" refusal. Every other character (NBSP, `/`, `'`, `+`, `-`,
|
|
339
|
+
* non-ASCII) came back unchanged.
|
|
340
|
+
*/
|
|
341
|
+
const CHECKOUT_NAME_LOOKALIKES: Readonly<Record<string, string>> = {
|
|
342
|
+
" ": " ",
|
|
343
|
+
".": "․",
|
|
344
|
+
"[": "(",
|
|
345
|
+
"]": ")",
|
|
346
|
+
"&": "&",
|
|
347
|
+
'"': "”",
|
|
348
|
+
"|": "¦",
|
|
349
|
+
};
|
|
350
|
+
const CHECKOUT_NAME_REWRITTEN = /[ .[\]&"|]/g;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The name a cart field is emitted AND signed under. The checkout store validates this exact
|
|
354
|
+
* string, and the shopper's cart line shows it, so "Nic Strength" stays readable as
|
|
355
|
+
* "Nic Strength" (with a no-break space) instead of failing as `Nic_Strength`.
|
|
356
|
+
*/
|
|
357
|
+
export function checkoutFieldName(name: string): string {
|
|
358
|
+
return name.trim().replace(CHECKOUT_NAME_REWRITTEN, (ch) => CHECKOUT_NAME_LOOKALIKES[ch] ?? ch);
|
|
359
|
+
}
|
|
360
|
+
|
|
332
361
|
/**
|
|
333
362
|
* How a field emitted by `cartFieldsForVariant` reaches the shopper. A field
|
|
334
363
|
* that is none of native, internal, declared-visible, or one of the variant's
|
|
335
364
|
* own options is `undeclared`: it would render on the cart line unreviewed.
|
|
365
|
+
* `name` is the emitted name (`checkoutFieldName`).
|
|
336
366
|
*/
|
|
337
367
|
export function cartFieldVisibility(name: string, variant: Pick<CatalogVariant, "option_values">): CartFieldVisibility {
|
|
338
368
|
if ((NATIVE_CART_FIELDS as readonly string[]).includes(name)) return "native";
|
|
339
369
|
if ((INTERNAL_CART_FIELDS as readonly string[]).includes(name)) return "internal";
|
|
340
|
-
if ((
|
|
341
|
-
if (Object.
|
|
370
|
+
if (VISIBLE_CART_FIELDS.some((visible) => checkoutFieldName(visible) === name)) return "visible";
|
|
371
|
+
if (Object.keys(variant.option_values).some((option) => checkoutFieldName(option) === name)) return "variant-option";
|
|
342
372
|
return "undeclared";
|
|
343
373
|
}
|
|
344
374
|
|
|
@@ -476,7 +506,9 @@ export function cartFieldsForVariant(
|
|
|
476
506
|
// Variant options (Flavor, Nicotine strength, …) travel as product options so
|
|
477
507
|
// the cart line reflects the exact variant the shopper picked.
|
|
478
508
|
for (const [optName, optVal] of Object.entries(variant.option_values)) {
|
|
479
|
-
|
|
509
|
+
const name = checkoutFieldName(optName);
|
|
510
|
+
const value = optVal.trim();
|
|
511
|
+
if (name && value) fields.push({ name, value });
|
|
480
512
|
}
|
|
481
513
|
// Bind the SELECTED purchase option into the signed payload so a shopper can't
|
|
482
514
|
// switch one-time↔subscription (or between plans) after the form is signed —
|
|
@@ -486,7 +518,7 @@ export function cartFieldsForVariant(
|
|
|
486
518
|
// on every non-subscription cart. Any authored option (a real one-time plan or
|
|
487
519
|
// a subscription) is emitted and signed → modifying it fails validation.
|
|
488
520
|
if (purchaseOption && !isDefaultOneTimeOption(purchaseOption)) {
|
|
489
|
-
fields.push({ name: "Purchase option", value: purchaseOption.label });
|
|
521
|
+
fields.push({ name: checkoutFieldName("Purchase option"), value: purchaseOption.label });
|
|
490
522
|
}
|
|
491
523
|
if (purchaseOption?.kind === "subscription") {
|
|
492
524
|
fields.push(...subscriptionCartFields(purchaseOption));
|
|
@@ -617,8 +649,14 @@ function constantTimeHexEqual(expected: string, presented: string): boolean {
|
|
|
617
649
|
return acc === 0;
|
|
618
650
|
}
|
|
619
651
|
|
|
652
|
+
/** A field as the checkout receives it unsigned: under its `checkoutFieldName`. */
|
|
653
|
+
function unsignedField(field: CartField): RenderedCartField {
|
|
654
|
+
return { name: checkoutFieldName(field.name), value: field.value };
|
|
655
|
+
}
|
|
656
|
+
|
|
620
657
|
/**
|
|
621
|
-
* Sign one field for product `code`.
|
|
658
|
+
* Sign one field for product `code`. The hashed name and the emitted name are the
|
|
659
|
+
* same `checkoutFieldName`, so they cannot diverge. Editable fields hash the sentinel
|
|
622
660
|
* `--OPEN--` and gain the `||open` suffix so checkout accepts a user-entered value.
|
|
623
661
|
*/
|
|
624
662
|
async function signField(
|
|
@@ -626,11 +664,12 @@ async function signField(
|
|
|
626
664
|
code: string,
|
|
627
665
|
field: CartField,
|
|
628
666
|
): Promise<RenderedCartField> {
|
|
667
|
+
const fieldName = checkoutFieldName(field.name);
|
|
629
668
|
const valueToHash = field.open ? "--OPEN--" : field.value;
|
|
630
|
-
const hash = await hmacHex(secret, code +
|
|
669
|
+
const hash = await hmacHex(secret, code + fieldName + valueToHash);
|
|
631
670
|
const name = field.open
|
|
632
|
-
? `${
|
|
633
|
-
: `${
|
|
671
|
+
? `${fieldName}||${hash}||open`
|
|
672
|
+
: `${fieldName}||${hash}`;
|
|
634
673
|
return { name, value: field.value };
|
|
635
674
|
}
|
|
636
675
|
|
|
@@ -673,7 +712,7 @@ export async function buildVariantCartPayload(
|
|
|
673
712
|
|
|
674
713
|
const rendered: RenderedCartField[] = signed
|
|
675
714
|
? await Promise.all(fields.map((f) => signField(opts.secret!, code, f)))
|
|
676
|
-
: fields.map(
|
|
715
|
+
: fields.map(unsignedField);
|
|
677
716
|
|
|
678
717
|
// Split the quantity field out: the island puts it on the visible stepper.
|
|
679
718
|
const qtyIdx = fields.findIndex((f) => f.name === "quantity");
|
|
@@ -861,7 +900,7 @@ export async function withVerificationRef(
|
|
|
861
900
|
const code = fields.find((field) => field.name.split("||")[0] === "code")?.value;
|
|
862
901
|
if (!code) return [...fields];
|
|
863
902
|
const field: CartField = { name: "verification", value: opts.ref };
|
|
864
|
-
if (opts.mode !== "signed") return [...fields, field];
|
|
903
|
+
if (opts.mode !== "signed") return [...fields, unsignedField(field)];
|
|
865
904
|
if (!opts.secret) throw new CartSigningUnavailableError();
|
|
866
905
|
return [...fields, await signField(opts.secret, code, field)];
|
|
867
906
|
}
|
|
@@ -883,7 +922,7 @@ export async function withShippingPolicy(
|
|
|
883
922
|
const currency = kept.find((field) => field.name.split("||")[0] === "currency")?.value;
|
|
884
923
|
if (!code || !currency) throw new ShippingPolicyUnavailableError();
|
|
885
924
|
const field: CartField = { name: SHIPPING_POLICY_CART_FIELD, value: shippingPolicyOptionValue(opts.policy, currency) };
|
|
886
|
-
if (opts.mode !== "signed") return [...kept, field];
|
|
925
|
+
if (opts.mode !== "signed") return [...kept, unsignedField(field)];
|
|
887
926
|
if (!opts.secret) throw new CartSigningUnavailableError();
|
|
888
927
|
return [...kept, await signField(opts.secret, code, field)];
|
|
889
928
|
}
|