@tokenoftrust/cli 3.7.3 → 3.7.5

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/cli",
3
- "version": "3.7.3",
3
+ "version": "3.7.5",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -291,6 +291,8 @@ export function normalizeShipResult(data) {
291
291
  artifactDigest: typeof o.artifactDigest === "string" ? o.artifactDigest : null,
292
292
  receiptId: typeof o.receiptId === "string" ? o.receiptId : null,
293
293
  rollbackTarget: o.rollbackTarget && typeof o.rollbackTarget === "object" ? o.rollbackTarget : null,
294
+ retriable: o.retriable === false ? false : o.retriable === true ? true : null,
295
+ pointerWritten: o.pointerWritten === true,
294
296
  };
295
297
  }
296
298
 
@@ -699,20 +701,34 @@ function reportShipResult(result, { tenant, liveUrl, noOpen, openUrl }) {
699
701
  return 1;
700
702
  }
701
703
  if (result.state === "promote_failed" || result.state === "record_failed") {
704
+ // A deterministic refusal (the live flip's compare-and-set, or any outcome that says a retry
705
+ // cannot change it) must not be offered as safe to retry.
706
+ const deterministic = result.retriable === false || result.reason === "live_moved";
702
707
  console.error(
703
708
  fail(
704
709
  result.message || `ship ${result.state.replace("_", " ")}`,
705
- "re-run `tot ship` — the ship is idempotent and safe to retry",
710
+ deterministic
711
+ ? "do not re-run `tot ship` — this refusal is deterministic and will not change"
712
+ : "re-run `tot ship` — the ship is idempotent and safe to retry",
706
713
  ),
707
714
  );
708
715
  return 1;
709
716
  }
710
717
  if (result.state === "publishing") {
711
- console.log(`\n ⏳ ${tenant} is publishing — the live pointer moved, CDN delivery is still confirming.`);
718
+ // Dispatched is not written. Say the pointer moved only once this result says it was.
719
+ console.log(
720
+ result.pointerWritten
721
+ ? `\n ⏳ ${tenant} is publishing — the live pointer moved, CDN delivery is still confirming.`
722
+ : `\n ⏳ ${tenant} is publishing — dispatched; the live pointer is not written yet.`,
723
+ );
712
724
  if (result.pinnedSha) console.log(` pinned: ${result.pinnedSha}`);
713
725
  if (result.receiptId) console.log(` receipt: ${result.receiptId}`);
714
726
  if (result.message) console.log(` ${result.message}`);
715
- console.log(" re-run `tot ship` in a bit to confirm — it dedupes onto this same dispatch, safe to retry.");
727
+ console.log(
728
+ result.pointerWritten
729
+ ? " re-run `tot ship` in a bit to confirm — it dedupes onto this same dispatch, safe to retry."
730
+ : " re-run `tot ship` in a bit to confirm — it dedupes onto this same dispatch.",
731
+ );
716
732
  return 1;
717
733
  }
718
734
  console.error(
@@ -23,7 +23,10 @@ export const CURRENCY_CODES: readonly string[];
23
23
  export function resolveStoreCurrency(declared: unknown): string;
24
24
  export function currencyDeclarationProblems(currency: unknown): string[];
25
25
  export const SHIPPING_KEYS: readonly string[];
26
+ export const SHIPPING_TIER_KEYS: readonly string[];
26
27
  export const MAX_SHIPPING_AMOUNT: number;
28
+ export const MAX_SHIPPING_TIERS: number;
29
+ export const MAX_SHIPPING_LABEL_LENGTH: number;
27
30
  export function shippingDeclarationProblems(shipping: unknown): string[];
28
31
  export const COMPLIANCE_NOTICES: Record<string, string>;
29
32
  export const MANDATED_NOTICE_KEYS: string[];
@@ -197,20 +197,38 @@ export function seoDeclarationProblems(seo) {
197
197
  }
198
198
 
199
199
  /** Every `shipping` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
200
- export const SHIPPING_KEYS = Object.freeze(["flatRate", "freeAtOrAbove"]);
200
+ export const SHIPPING_KEYS = Object.freeze(["tiers"]);
201
+
202
+ /** Every key of one `shipping.tiers` entry. */
203
+ export const SHIPPING_TIER_KEYS = Object.freeze(["atOrAbove", "rate", "label"]);
201
204
 
202
205
  /** Largest shipping amount a declaration may state, in the cart's currency. */
203
206
  export const MAX_SHIPPING_AMOUNT = 100000;
204
207
 
208
+ /** Most tiers one policy may declare. */
209
+ export const MAX_SHIPPING_TIERS = 10;
210
+
211
+ /** Longest shipping-line label, in characters. */
212
+ export const MAX_SHIPPING_LABEL_LENGTH = 100;
213
+
205
214
  /** @param {unknown} value */
206
215
  const isShippingAmount = (value) =>
207
216
  typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= MAX_SHIPPING_AMOUNT
208
217
  && Math.abs(value * 100 - Math.round(value * 100)) < 1e-6;
209
218
 
219
+ /** @param {unknown} value */
220
+ const isShippingLabel = (value) =>
221
+ typeof value === "string" && value.trim() === value && value.length > 0
222
+ && [...value].length <= MAX_SHIPPING_LABEL_LENGTH && !/[\u0000-\u001f\u007f]/.test(value);
223
+
210
224
  /**
211
225
  * What is wrong with a declared `shipping` value, one sentence per problem; `[]` when it is
212
226
  * valid. A policy with any problem is refused whole, so checkout never charges half of one.
213
227
  *
228
+ * Tiers are ordered by the item subtotal they start at. The first starts at 0 and states no
229
+ * `atOrAbove`; each later tier's `atOrAbove` is above the one before it and is inclusive, so an
230
+ * order whose subtotal equals it pays that tier's `rate`.
231
+ *
214
232
  * @param {unknown} shipping
215
233
  * @returns {string[]}
216
234
  */
@@ -221,12 +239,38 @@ export function shippingDeclarationProblems(shipping) {
221
239
  for (const key of Object.keys(source)) {
222
240
  if (!SHIPPING_KEYS.includes(key)) problems.push(`\`shipping.${key}\` is not a shipping key (${SHIPPING_KEYS.join(", ")})`);
223
241
  }
224
- if (!isShippingAmount(source.flatRate)) {
225
- problems.push(`\`shipping.flatRate\` must be an amount from 0 to ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
226
- }
227
- if (source.freeAtOrAbove !== undefined && !(isShippingAmount(source.freeAtOrAbove) && /** @type {number} */ (source.freeAtOrAbove) > 0)) {
228
- problems.push(`\`shipping.freeAtOrAbove\` must be an amount above 0 and at most ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
242
+ const tiers = source.tiers;
243
+ if (!Array.isArray(tiers) || tiers.length === 0 || tiers.length > MAX_SHIPPING_TIERS) {
244
+ problems.push(`\`shipping.tiers\` must be a list of 1 to ${MAX_SHIPPING_TIERS} tiers`);
245
+ return problems;
229
246
  }
247
+ let previous = 0;
248
+ tiers.forEach((tier, index) => {
249
+ const at = `shipping.tiers[${index}]`;
250
+ if (!isPlainObject(tier)) {
251
+ problems.push(`\`${at}\` must be an object`);
252
+ return;
253
+ }
254
+ const entry = /** @type {Record<string, unknown>} */ (tier);
255
+ for (const key of Object.keys(entry)) {
256
+ if (!SHIPPING_TIER_KEYS.includes(key)) problems.push(`\`${at}.${key}\` is not a tier key (${SHIPPING_TIER_KEYS.join(", ")})`);
257
+ }
258
+ if (!isShippingAmount(entry.rate)) {
259
+ problems.push(`\`${at}.rate\` must be an amount from 0 to ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
260
+ }
261
+ if (entry.label !== undefined && !isShippingLabel(entry.label)) {
262
+ problems.push(`\`${at}.label\` must be text of 1 to ${MAX_SHIPPING_LABEL_LENGTH} characters with no surrounding spaces or line breaks`);
263
+ }
264
+ if (index === 0) {
265
+ if (entry.atOrAbove !== undefined) problems.push(`\`${at}\` starts at 0, so it states no \`atOrAbove\``);
266
+ return;
267
+ }
268
+ if (!(isShippingAmount(entry.atOrAbove) && /** @type {number} */ (entry.atOrAbove) > previous)) {
269
+ problems.push(`\`${at}.atOrAbove\` must be an amount above ${previous}, where the tier before it starts, and at most ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
270
+ return;
271
+ }
272
+ previous = /** @type {number} */ (entry.atOrAbove);
273
+ });
230
274
  return problems;
231
275
  }
232
276
 
@@ -702,7 +746,7 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
702
746
  const problems = shippingDeclarationProblems(config.shipping);
703
747
  if (problems.length > 0) {
704
748
  out.push(mk(ERROR, "config-shipping", `${file} shipping`, shippingProblemMessage(problems),
705
- 'declare e.g. `"shipping": { "flatRate": 5.99, "freeAtOrAbove": 100 }`, or remove `shipping` to charge none'));
749
+ 'declare e.g. `"shipping": { "tiers": [ { "rate": 5.99 }, { "atOrAbove": 100, "rate": 0, "label": "Free shipping" } ] }`, or remove `shipping` to charge none'));
706
750
  }
707
751
  }
708
752
  return out;