@asteby/storefront 0.16.1 → 0.18.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/dist/index.cjs CHANGED
@@ -124,9 +124,14 @@ function appendParams(search, params) {
124
124
  return search;
125
125
  }
126
126
  function productListQuery(params = {}) {
127
- const { filters, handles, ...rest } = params;
127
+ const { filters, handles, tags_any, ...rest } = params;
128
128
  const search = appendParams(new URLSearchParams(), rest);
129
129
  if (handles && handles.length > 0) search.set("handles", handles.join(","));
130
+ if (tags_any && tags_any.length > 0) {
131
+ if (tags_any.length > 5) throw new RangeError("tags_any takes at most 5 tags");
132
+ if (tags_any.some((t) => t.includes(",") || t.length > 120)) throw new TypeError("tags_any: a tag has at most 120 characters and no comma");
133
+ search.set("tags_any", tags_any.join(","));
134
+ }
130
135
  if (filters) {
131
136
  for (const [key, raw] of Object.entries(filters)) {
132
137
  const name = key.startsWith("filter.") ? key : `filter.${key}`;
@@ -486,6 +491,43 @@ function createStorefrontClient(config) {
486
491
  get: (options) => get("/storefront/customer", void 0, options),
487
492
  /** Updates the profile and the saved fiscal data; `fiscal: null` deletes the latter. `422` with `field: 'fiscal.<field>'`. */
488
493
  update: (input, options) => write("PATCH", "/storefront/customer", input, options),
494
+ /**
495
+ * Active sessions of the buyer («Devices»; contract 1.9). Private, never cached. The list carries no address
496
+ * and no token: only what the buyer needs to recognize their own devices.
497
+ */
498
+ sessions: {
499
+ list: async (options) => (await get("/storefront/customer/sessions", void 0, { ...options, cache: "no-store" })).nodes,
500
+ /** Closes one session. Repeating it is fine. `404` for a session that is not the buyer's. Closing the current one is a sign-out. */
501
+ revoke: (id, options) => write("DELETE", `/storefront/customer/sessions/${enc(id)}`, void 0, options),
502
+ /** Closes every session except the one making the call. */
503
+ revokeOthers: (options) => write("DELETE", "/storefront/customer/sessions/others", void 0, options)
504
+ },
505
+ /**
506
+ * Confirms the buyer's identity again with their password or a sign-in code (`requestOtp` first). The
507
+ * confirmation lasts `valid_for_seconds` (a few minutes) and counts only for the session that made the call;
508
+ * exporting data and deleting the account need it (`403 reauth_required` otherwise, see `isReauthRequired`).
509
+ * A wrong credential is always the same `400`; too many tries is `429` (`StorefrontError.retryAfter`).
510
+ */
511
+ reauth: (input, options) => write("POST", "/storefront/customer/reauth", input, options),
512
+ /**
513
+ * The buyer's data as a JSON file (contract 1.9, `capabilities().data_export`). Needs a recent `reauth`. It is
514
+ * built on request and is neither stored nor emailed: call it from YOUR server, stream the bytes
515
+ * (`dataExportBytes`, `safeExportFilename`) with `Cache-Control: no-store`, and keep nothing. Three a day per account.
516
+ */
517
+ exportData: async (options) => {
518
+ const file = await write("POST", "/storefront/customer/data-export", void 0, { ...options, cache: "no-store" });
519
+ if (!file || typeof file.content_base64 !== "string" || typeof file.filename !== "string" || typeof file.content_type !== "string") {
520
+ throw new StorefrontError({ status: 502, code: "invalid_response", message: "The data export answer is not a file" });
521
+ }
522
+ return { filename: file.filename, content_type: file.content_type, content_base64: file.content_base64 };
523
+ },
524
+ /**
525
+ * Deletes the buyer's account (contract 1.9, `capabilities().account_delete`). Needs a recent `reauth` and the
526
+ * text the buyer typed in `confirm`. Personal data is erased; orders and invoices are kept by the store.
527
+ * `409` (see `isAccountDeleteBlocked`) while an order is on its way or a payment is pending. Idempotent: a
528
+ * repeat after success is also fine. Afterwards the token is dead: drop it.
529
+ */
530
+ deleteAccount: (input, options) => write("DELETE", "/storefront/customer", input, options),
489
531
  orders: (params, options) => get("/storefront/customer/orders", params, options),
490
532
  /** `null` when the order does not exist or belongs to someone else. */
491
533
  order: (id, options) => getOrNull(`/storefront/customer/orders/${enc(id)}`, void 0, options),
@@ -536,6 +578,19 @@ function createStorefrontClient(config) {
536
578
  /** Physical delete (a profile leaves nothing behind). A saved default passes to another profile. */
537
579
  delete: (id, options) => write("DELETE", `/storefront/customer/fiscal-profiles/${enc(id)}`, void 0, options)
538
580
  },
581
+ /**
582
+ * Saved items of the buyer («My items», «My garage»; contract 1.9, `capabilities().subjects`). Personal data:
583
+ * private, never cached, never logged. `422` with `field: 'attributes.<field>'` when the kind refuses a value
584
+ * (it never echoes it); `409` for an item already saved or past the limit; `429` for too many saves; `404` for
585
+ * an item that is not the buyer's. The server builds `label` and `fit_key`; private fields come back masked.
586
+ */
587
+ subjects: {
588
+ list: async (params, options) => (await get("/storefront/customer/subjects", params, { ...options, cache: "no-store" })).nodes,
589
+ create: (input, options) => write("POST", "/storefront/customer/subjects", input, options),
590
+ update: (id, patch, options) => write("PATCH", `/storefront/customer/subjects/${enc(id)}`, patch, options),
591
+ /** Physical delete (the item leaves nothing behind). The default of its kind passes to another item. */
592
+ delete: (id, options) => write("DELETE", `/storefront/customer/subjects/${enc(id)}`, void 0, options)
593
+ },
539
594
  addresses: {
540
595
  list: (options) => get("/storefront/customer/addresses", void 0, options),
541
596
  create: (address, options) => write("POST", "/storefront/customer/addresses", address, options),
@@ -544,6 +599,11 @@ function createStorefrontClient(config) {
544
599
  }
545
600
  },
546
601
  content: {
602
+ /**
603
+ * The kinds of buyer item the organization declares («My garage») and their catalogs (contract 1.9; public,
604
+ * cacheable). `[]` when it declares none or the server predates the route: the store has no such section.
605
+ */
606
+ subjectKinds: async (options) => (await getOrNull("/storefront/subject-kinds", void 0, options))?.nodes ?? [],
547
607
  theme: (options) => get("/storefront/content/theme", void 0, options),
548
608
  /** `null` when the page does not exist. */
549
609
  page: (handle, options) => getOrNull(`/storefront/content/pages/${enc(handle)}`, previewQuery(options), options),
@@ -1451,6 +1511,29 @@ function shipmentsStatus(order) {
1451
1511
  return live.length ? live[0].status : null;
1452
1512
  }
1453
1513
 
1514
+ // src/account-settings.ts
1515
+ function isReauthRequired(error) {
1516
+ const e = error;
1517
+ return !!e && typeof e === "object" && e.status === 403 && e.code === "reauth_required";
1518
+ }
1519
+ function isAccountDeleteBlocked(error) {
1520
+ const e = error;
1521
+ return !!e && typeof e === "object" && e.status === 409 && (e.field === "orders" || e.field === "pending_payments");
1522
+ }
1523
+ function dataExportBytes(file) {
1524
+ const raw = typeof file.content_base64 === "string" ? file.content_base64.replace(/\s+/g, "") : "";
1525
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(raw) || raw.length % 4 === 1) throw new TypeError("data export is not valid base64");
1526
+ const bin = atob(raw);
1527
+ const out = new Uint8Array(bin.length);
1528
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1529
+ return out;
1530
+ }
1531
+ function safeExportFilename(name, fallback = "account-data.json") {
1532
+ const base = typeof name === "string" ? name.split(/[\\/]/).pop() ?? "" : "";
1533
+ const clean = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "").slice(0, 80);
1534
+ return clean || fallback;
1535
+ }
1536
+
1454
1537
  // src/invoice-docs.ts
1455
1538
  function groupInvoiceDocs(docs) {
1456
1539
  const list = Array.isArray(docs) ? docs.filter((d) => !!d && typeof d === "object") : [];
@@ -1478,6 +1561,199 @@ function safeInvoiceFilename(name, fallback = "factura") {
1478
1561
  return clean || fallback;
1479
1562
  }
1480
1563
 
1564
+ // src/subjects.ts
1565
+ var MIN_SUBJECT_YEAR = 1900;
1566
+ var MAX_SUBJECT_YEAR = 2100;
1567
+ var MAX_VALUE_LENGTH = 80;
1568
+ var MAX_LABEL_LENGTH = 120;
1569
+ var MAX_FIT_KEY_LENGTH = 100;
1570
+ var isPresent = (v) => v !== null && v !== void 0 && v !== "";
1571
+ var asYear = (v) => {
1572
+ const n = typeof v === "number" ? v : typeof v === "string" && /^\s*\d+\s*$/.test(v) ? Number(v) : NaN;
1573
+ return Number.isInteger(n) && n >= MIN_SUBJECT_YEAR && n <= MAX_SUBJECT_YEAR ? n : null;
1574
+ };
1575
+ var cleanText = (v, max) => {
1576
+ if (typeof v !== "string") return null;
1577
+ const s = v.split(/\s+/).filter(Boolean).join(" ");
1578
+ return [...s].length > max || /[\u0000-\u001f\u007f-\u009f]/.test(s) ? null : s;
1579
+ };
1580
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9._+-]+/g, "-").replace(/^-+|-+$/g, "");
1581
+ function coversYear(node, year) {
1582
+ if (node.ranges && node.ranges.length > 0) return node.ranges.some(([a, b]) => year >= a && year <= b);
1583
+ if (node.from !== void 0 && year < node.from) return false;
1584
+ if (node.to !== void 0 && year > node.to) return false;
1585
+ return true;
1586
+ }
1587
+ var declaresYears = (n) => n.from !== void 0 || n.to !== void 0 || (n.ranges?.length ?? 0) > 0;
1588
+ function subjectLevels(kind) {
1589
+ return kind.fields.filter((f) => f.source === "catalog" && f.type !== "year");
1590
+ }
1591
+ function subjectFields(kind) {
1592
+ return kind.fields;
1593
+ }
1594
+ function walk(kind, values) {
1595
+ const path = [];
1596
+ const ids = {};
1597
+ let tree = kind.catalog?.tree;
1598
+ for (const f of subjectLevels(kind)) {
1599
+ const raw = values[f.key];
1600
+ const s = isPresent(raw) ? cleanText(raw, MAX_VALUE_LENGTH) : null;
1601
+ const node = s && tree ? tree[s] ?? tree[s.toLowerCase()] : void 0;
1602
+ if (!s || !node) return { path, ids, next: tree };
1603
+ ids[f.key] = tree && tree[s] ? s : s.toLowerCase();
1604
+ path.push(node);
1605
+ tree = node.children;
1606
+ if (!tree) return { path, ids, next: void 0 };
1607
+ }
1608
+ return { path, ids, next: tree };
1609
+ }
1610
+ var byLabel = (a, b) => a.label.localeCompare(b.label, void 0, { sensitivity: "base", numeric: true });
1611
+ function subjectOptions(kind, fieldKey, values) {
1612
+ const field = kind.fields.find((f) => f.key === fieldKey);
1613
+ if (!field || field.source !== "catalog" || !kind.catalog) return [];
1614
+ if (field.type === "year") {
1615
+ const upto = kind.fields.slice(0, kind.fields.indexOf(field));
1616
+ const { path: path2 } = walk({ ...kind, fields: upto }, values);
1617
+ const declared = path2.filter(declaresYears);
1618
+ if (declared.length === 0) return [];
1619
+ let lo = Infinity;
1620
+ let hi = -Infinity;
1621
+ for (const n of declared) {
1622
+ for (const [a, b] of n.ranges?.length ? n.ranges : [[n.from ?? MIN_SUBJECT_YEAR, n.to ?? MAX_SUBJECT_YEAR]]) {
1623
+ lo = Math.min(lo, a);
1624
+ hi = Math.max(hi, b);
1625
+ }
1626
+ }
1627
+ const out = [];
1628
+ for (let y = Math.min(hi, MAX_SUBJECT_YEAR); y >= Math.max(lo, MIN_SUBJECT_YEAR); y--) if (declared.every((n) => coversYear(n, y))) out.push({ value: String(y), label: String(y) });
1629
+ return out;
1630
+ }
1631
+ const levels = subjectLevels(kind);
1632
+ const at = levels.findIndex((f) => f.key === fieldKey);
1633
+ if (at < 0) return [];
1634
+ const before = Object.fromEntries(levels.slice(0, at).map((f) => [f.key, values[f.key]]));
1635
+ const { path, next } = walk({ ...kind, fields: kind.fields.filter((f) => levels.slice(0, at).includes(f) || f.type === "year") }, before);
1636
+ if (path.length < at || !next) return [];
1637
+ const year = asYear(values[kind.fields.find((f) => f.type === "year" && f.source === "catalog")?.key ?? ""]);
1638
+ return Object.entries(next).filter(([, n]) => year === null || coversYear(n, year)).map(([value, n]) => ({ value, label: n.label })).sort(byLabel);
1639
+ }
1640
+ function reconcileSubjectValues(kind, values) {
1641
+ const out = { ...values };
1642
+ for (const f of kind.fields) {
1643
+ if (f.source !== "catalog" || !isPresent(out[f.key])) continue;
1644
+ const options = subjectOptions(kind, f.key, out);
1645
+ const v = String(out[f.key]);
1646
+ const ok = f.type === "year" ? options.length === 0 || options.some((o) => o.value === v) : options.some((o) => o.value === v || o.value === v.toLowerCase());
1647
+ if (!ok) out[f.key] = "";
1648
+ }
1649
+ return out;
1650
+ }
1651
+ function resolveSubject(kind, values) {
1652
+ const problems = {};
1653
+ const known = new Set(kind.fields.map((f) => f.key));
1654
+ for (const key2 of Object.keys(values)) if (isPresent(values[key2]) && !known.has(key2)) problems[key2] = "unknown";
1655
+ const attributes = {};
1656
+ const priv = {};
1657
+ const display = {};
1658
+ const ids = {};
1659
+ const path = [];
1660
+ let tree = kind.catalog?.tree;
1661
+ const years = [];
1662
+ const put = (f, v) => {
1663
+ if (f.private) priv[f.key] = String(v);
1664
+ else attributes[f.key] = v;
1665
+ };
1666
+ for (const f of kind.fields) {
1667
+ const raw = values[f.key];
1668
+ if (!isPresent(raw)) {
1669
+ if (f.required) problems[f.key] = "required";
1670
+ if (f.source === "catalog" && f.type !== "year") tree = void 0;
1671
+ continue;
1672
+ }
1673
+ const max = f.max ?? MAX_VALUE_LENGTH;
1674
+ if (f.type === "year") {
1675
+ const y = asYear(raw);
1676
+ if (y === null) {
1677
+ problems[f.key] = "invalid";
1678
+ continue;
1679
+ }
1680
+ put(f, y);
1681
+ display[f.key] = ids[f.key] = String(y);
1682
+ if (f.source === "catalog") years.push(f.key);
1683
+ } else if (f.source === "catalog") {
1684
+ if (!kind.catalog) {
1685
+ problems[f.key] = "catalog_unavailable";
1686
+ continue;
1687
+ }
1688
+ const s = cleanText(raw, max);
1689
+ if (s === null) {
1690
+ problems[f.key] = "invalid";
1691
+ continue;
1692
+ }
1693
+ const node = tree?.[s] ?? tree?.[s.toLowerCase()];
1694
+ if (!node) {
1695
+ problems[f.key] = "not_in_catalog";
1696
+ continue;
1697
+ }
1698
+ const id = tree?.[s] ? s : s.toLowerCase();
1699
+ path.push(node);
1700
+ tree = node.children;
1701
+ put(f, id);
1702
+ display[f.key] = node.label;
1703
+ ids[f.key] = id;
1704
+ } else {
1705
+ const s = cleanText(raw, max);
1706
+ if (s === null) {
1707
+ problems[f.key] = "invalid";
1708
+ continue;
1709
+ }
1710
+ put(f, s);
1711
+ display[f.key] = s;
1712
+ ids[f.key] = slug(s);
1713
+ }
1714
+ }
1715
+ for (const key2 of years) {
1716
+ const y = attributes[key2];
1717
+ if (y !== void 0 && !problems[key2] && !path.every((n) => coversYear(n, y))) problems[key2] = "year_range";
1718
+ }
1719
+ if (Object.keys(problems).length > 0) return { ok: false, problems };
1720
+ const label = [...kind.fields.filter((f) => !f.private).map((f) => display[f.key]).filter(Boolean).join(" ")].slice(0, MAX_LABEL_LENGTH).join("").trim();
1721
+ if (!label) return { ok: false, problems: { _: "required" } };
1722
+ let complete = true;
1723
+ const key = kind.fit_key_template.replace(/\{([a-z][a-z0-9_]*)\}/g, (_m, name) => {
1724
+ const id = ids[name];
1725
+ if (!id) complete = false;
1726
+ return id ?? "";
1727
+ });
1728
+ return { ok: true, subject: { label, fit_key: complete && key.length <= MAX_FIT_KEY_LENGTH ? key.toLowerCase() : "", attributes, private: priv } };
1729
+ }
1730
+ var PRIVATE_MASK = "\u2022\u2022";
1731
+ var isMasked = (v) => typeof v === "string" && v.startsWith(PRIVATE_MASK);
1732
+ var DEFAULT_FIT_TAG_PREFIX = "fit:";
1733
+ var fitTag = (fitKey, prefix = DEFAULT_FIT_TAG_PREFIX) => `${prefix}${fitKey}`;
1734
+ function fitTagsAny(fitKey, opts = {}) {
1735
+ if (!fitKey) return [];
1736
+ const prefix = opts.prefix ?? DEFAULT_FIT_TAG_PREFIX;
1737
+ return opts.universal === false ? [fitTag(fitKey, prefix)] : [fitTag(fitKey, prefix), `${prefix}universal`];
1738
+ }
1739
+ var SUBJECT_CATALOG_PAGE_BYTES = 28 * 1024;
1740
+ var utf8Bytes = (s) => new TextEncoder().encode(s).length;
1741
+ function splitSubjectTree(tree, maxBytes = SUBJECT_CATALOG_PAGE_BYTES) {
1742
+ const size = (part) => utf8Bytes(JSON.stringify(JSON.stringify(part)));
1743
+ const parts = [];
1744
+ let current = {};
1745
+ for (const [id, node] of Object.entries(tree)) {
1746
+ if (size({ [id]: node }) > maxBytes) throw new RangeError(`splitSubjectTree: option ${id} alone exceeds ${maxBytes} bytes`);
1747
+ const next = { ...current, [id]: node };
1748
+ if (Object.keys(current).length > 0 && size(next) > maxBytes) {
1749
+ parts.push(current);
1750
+ current = { [id]: node };
1751
+ } else current = next;
1752
+ }
1753
+ if (Object.keys(current).length > 0) parts.push(current);
1754
+ return parts;
1755
+ }
1756
+
1481
1757
  // src/delivery.ts
1482
1758
  function parseDelivery(value) {
1483
1759
  const raw = (value ?? "").trim();
@@ -1506,11 +1782,16 @@ function deliveryNotices(delivery) {
1506
1782
 
1507
1783
  exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
1508
1784
  exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
1785
+ exports.DEFAULT_FIT_TAG_PREFIX = DEFAULT_FIT_TAG_PREFIX;
1786
+ exports.MAX_SUBJECT_YEAR = MAX_SUBJECT_YEAR;
1787
+ exports.MIN_SUBJECT_YEAR = MIN_SUBJECT_YEAR;
1509
1788
  exports.MISSING_WEIGHT = MISSING_WEIGHT;
1510
1789
  exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
1511
1790
  exports.PAYMENT_CHOICE_REFUSED_FIELD = PAYMENT_CHOICE_REFUSED_FIELD;
1512
1791
  exports.PICKUP_STEPS = PICKUP_STEPS;
1792
+ exports.PRIVATE_MASK = PRIVATE_MASK;
1513
1793
  exports.SHIPPING_STEPS = SHIPPING_STEPS;
1794
+ exports.SUBJECT_CATALOG_PAGE_BYTES = SUBJECT_CATALOG_PAGE_BYTES;
1514
1795
  exports.StorefrontError = StorefrontError;
1515
1796
  exports.appendParams = appendParams;
1516
1797
  exports.availabilityLocale = availabilityLocale;
@@ -1519,12 +1800,16 @@ exports.availabilityView = availabilityView;
1519
1800
  exports.cartLineProblemView = cartLineProblemView;
1520
1801
  exports.clampPercent = clampPercent;
1521
1802
  exports.classifyPaymentError = classifyPaymentError;
1803
+ exports.coversYear = coversYear;
1522
1804
  exports.createStorefrontClient = createStorefrontClient;
1805
+ exports.dataExportBytes = dataExportBytes;
1523
1806
  exports.declineMessage = declineMessage;
1524
1807
  exports.declineMessageKey = declineMessageKey;
1525
1808
  exports.deliveryInput = deliveryInput;
1526
1809
  exports.deliveryNotices = deliveryNotices;
1527
1810
  exports.diagnose = diagnose;
1811
+ exports.fitTag = fitTag;
1812
+ exports.fitTagsAny = fitTagsAny;
1528
1813
  exports.formatAvailabilityMessage = formatAvailabilityMessage;
1529
1814
  exports.formatMoney = formatMoney;
1530
1815
  exports.formatVoucherExpiry = formatVoucherExpiry;
@@ -1532,6 +1817,9 @@ exports.generateIdempotencyKey = generateIdempotencyKey;
1532
1817
  exports.groupInvoiceDocs = groupInvoiceDocs;
1533
1818
  exports.hreflangLinks = hreflangLinks;
1534
1819
  exports.invoiceFileBytes = invoiceFileBytes;
1820
+ exports.isAccountDeleteBlocked = isAccountDeleteBlocked;
1821
+ exports.isMasked = isMasked;
1822
+ exports.isReauthRequired = isReauthRequired;
1535
1823
  exports.isStorefrontError = isStorefrontError;
1536
1824
  exports.isTestInvoice = isTestInvoice;
1537
1825
  exports.isTestShipment = isTestShipment;
@@ -1556,16 +1844,23 @@ exports.productAvailability = productAvailability;
1556
1844
  exports.productListQuery = productListQuery;
1557
1845
  exports.promoProgressView = promoProgressView;
1558
1846
  exports.promotionMessages = promotionMessages;
1847
+ exports.reconcileSubjectValues = reconcileSubjectValues;
1559
1848
  exports.repairPath = repairPath;
1560
1849
  exports.resolveAvailability = resolveAvailability;
1850
+ exports.resolveSubject = resolveSubject;
1561
1851
  exports.rewardLabel = rewardLabel;
1852
+ exports.safeExportFilename = safeExportFilename;
1562
1853
  exports.safeHttpsUrl = safeHttpsUrl;
1563
1854
  exports.safeInvoiceFilename = safeInvoiceFilename;
1564
1855
  exports.safeTrackingUrl = safeTrackingUrl;
1565
1856
  exports.schemaAvailability = schemaAvailability;
1566
1857
  exports.shipmentsStatus = shipmentsStatus;
1567
1858
  exports.sortTrackingEvents = sortTrackingEvents;
1859
+ exports.splitSubjectTree = splitSubjectTree;
1568
1860
  exports.storefrontDomainHeader = storefrontDomainHeader;
1861
+ exports.subjectFields = subjectFields;
1862
+ exports.subjectLevels = subjectLevels;
1863
+ exports.subjectOptions = subjectOptions;
1569
1864
  exports.suggestMarket = suggestMarket;
1570
1865
  exports.toMoney = toMoney;
1571
1866
  exports.visitorCountry = visitorCountry;