@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/README.md +24 -0
- package/dist/{types-DSHzYSHU.d.ts → account-settings-Cw3PamUA.d.ts} +241 -1
- package/dist/{types-C5nEQicj.d.cts → account-settings-DAzmXHIn.d.cts} +241 -1
- package/dist/{chunk-IPUAZ3LQ.js → chunk-5BJRZIS3.js} +196 -3
- package/dist/chunk-5BJRZIS3.js.map +1 -0
- package/dist/cli.js +62 -2
- package/dist/fiscal-document.d.cts +2 -2
- package/dist/fiscal-document.d.ts +2 -2
- package/dist/index.cjs +296 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -70
- package/dist/index.d.ts +135 -70
- package/dist/index.js +87 -4
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +403 -3
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +33 -2
- package/dist/mock.d.ts +33 -2
- package/dist/mock.js +299 -5
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IPUAZ3LQ.js.map +0 -1
package/dist/mock.cjs
CHANGED
|
@@ -1347,6 +1347,111 @@ function maskTaxId(taxId) {
|
|
|
1347
1347
|
return v.length <= 4 ? "*".repeat(v.length) : `${v.slice(0, 2)}${"*".repeat(v.length - 5)}${v.slice(-3)}`;
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
|
+
// src/subjects.ts
|
|
1351
|
+
var MIN_SUBJECT_YEAR = 1900;
|
|
1352
|
+
var MAX_SUBJECT_YEAR = 2100;
|
|
1353
|
+
var MAX_VALUE_LENGTH = 80;
|
|
1354
|
+
var MAX_LABEL_LENGTH = 120;
|
|
1355
|
+
var MAX_FIT_KEY_LENGTH = 100;
|
|
1356
|
+
var isPresent = (v) => v !== null && v !== void 0 && v !== "";
|
|
1357
|
+
var asYear = (v) => {
|
|
1358
|
+
const n = typeof v === "number" ? v : typeof v === "string" && /^\s*\d+\s*$/.test(v) ? Number(v) : NaN;
|
|
1359
|
+
return Number.isInteger(n) && n >= MIN_SUBJECT_YEAR && n <= MAX_SUBJECT_YEAR ? n : null;
|
|
1360
|
+
};
|
|
1361
|
+
var cleanText = (v, max) => {
|
|
1362
|
+
if (typeof v !== "string") return null;
|
|
1363
|
+
const s = v.split(/\s+/).filter(Boolean).join(" ");
|
|
1364
|
+
return [...s].length > max || /[\u0000-\u001f\u007f-\u009f]/.test(s) ? null : s;
|
|
1365
|
+
};
|
|
1366
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9._+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1367
|
+
function coversYear(node, year) {
|
|
1368
|
+
if (node.ranges && node.ranges.length > 0) return node.ranges.some(([a, b]) => year >= a && year <= b);
|
|
1369
|
+
if (node.from !== void 0 && year < node.from) return false;
|
|
1370
|
+
if (node.to !== void 0 && year > node.to) return false;
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
function resolveSubject(kind, values) {
|
|
1374
|
+
const problems = {};
|
|
1375
|
+
const known = new Set(kind.fields.map((f) => f.key));
|
|
1376
|
+
for (const key2 of Object.keys(values)) if (isPresent(values[key2]) && !known.has(key2)) problems[key2] = "unknown";
|
|
1377
|
+
const attributes = {};
|
|
1378
|
+
const priv = {};
|
|
1379
|
+
const display = {};
|
|
1380
|
+
const ids = {};
|
|
1381
|
+
const path = [];
|
|
1382
|
+
let tree = kind.catalog?.tree;
|
|
1383
|
+
const years = [];
|
|
1384
|
+
const put = (f, v) => {
|
|
1385
|
+
if (f.private) priv[f.key] = String(v);
|
|
1386
|
+
else attributes[f.key] = v;
|
|
1387
|
+
};
|
|
1388
|
+
for (const f of kind.fields) {
|
|
1389
|
+
const raw = values[f.key];
|
|
1390
|
+
if (!isPresent(raw)) {
|
|
1391
|
+
if (f.required) problems[f.key] = "required";
|
|
1392
|
+
if (f.source === "catalog" && f.type !== "year") tree = void 0;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
const max = f.max ?? MAX_VALUE_LENGTH;
|
|
1396
|
+
if (f.type === "year") {
|
|
1397
|
+
const y = asYear(raw);
|
|
1398
|
+
if (y === null) {
|
|
1399
|
+
problems[f.key] = "invalid";
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
put(f, y);
|
|
1403
|
+
display[f.key] = ids[f.key] = String(y);
|
|
1404
|
+
if (f.source === "catalog") years.push(f.key);
|
|
1405
|
+
} else if (f.source === "catalog") {
|
|
1406
|
+
if (!kind.catalog) {
|
|
1407
|
+
problems[f.key] = "catalog_unavailable";
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const s = cleanText(raw, max);
|
|
1411
|
+
if (s === null) {
|
|
1412
|
+
problems[f.key] = "invalid";
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const node = tree?.[s] ?? tree?.[s.toLowerCase()];
|
|
1416
|
+
if (!node) {
|
|
1417
|
+
problems[f.key] = "not_in_catalog";
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
const id = tree?.[s] ? s : s.toLowerCase();
|
|
1421
|
+
path.push(node);
|
|
1422
|
+
tree = node.children;
|
|
1423
|
+
put(f, id);
|
|
1424
|
+
display[f.key] = node.label;
|
|
1425
|
+
ids[f.key] = id;
|
|
1426
|
+
} else {
|
|
1427
|
+
const s = cleanText(raw, max);
|
|
1428
|
+
if (s === null) {
|
|
1429
|
+
problems[f.key] = "invalid";
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
put(f, s);
|
|
1433
|
+
display[f.key] = s;
|
|
1434
|
+
ids[f.key] = slug(s);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (const key2 of years) {
|
|
1438
|
+
const y = attributes[key2];
|
|
1439
|
+
if (y !== void 0 && !problems[key2] && !path.every((n) => coversYear(n, y))) problems[key2] = "year_range";
|
|
1440
|
+
}
|
|
1441
|
+
if (Object.keys(problems).length > 0) return { ok: false, problems };
|
|
1442
|
+
const label = [...kind.fields.filter((f) => !f.private).map((f) => display[f.key]).filter(Boolean).join(" ")].slice(0, MAX_LABEL_LENGTH).join("").trim();
|
|
1443
|
+
if (!label) return { ok: false, problems: { _: "required" } };
|
|
1444
|
+
let complete = true;
|
|
1445
|
+
const key = kind.fit_key_template.replace(/\{([a-z][a-z0-9_]*)\}/g, (_m, name) => {
|
|
1446
|
+
const id = ids[name];
|
|
1447
|
+
if (!id) complete = false;
|
|
1448
|
+
return id ?? "";
|
|
1449
|
+
});
|
|
1450
|
+
return { ok: true, subject: { label, fit_key: complete && key.length <= MAX_FIT_KEY_LENGTH ? key.toLowerCase() : "", attributes, private: priv } };
|
|
1451
|
+
}
|
|
1452
|
+
var PRIVATE_MASK = "\u2022\u2022";
|
|
1453
|
+
var isMasked = (v) => typeof v === "string" && v.startsWith(PRIVATE_MASK);
|
|
1454
|
+
|
|
1350
1455
|
// src/mock/seed.ts
|
|
1351
1456
|
var MOCK_IMAGE_URL = (seed, width = 800, height = 800) => `https://picsum.photos/seed/${encodeURIComponent(seed)}/${width}/${height}`;
|
|
1352
1457
|
var MOCK_CURRENCY = "MXN";
|
|
@@ -1683,6 +1788,55 @@ function buildCustomers() {
|
|
|
1683
1788
|
];
|
|
1684
1789
|
}
|
|
1685
1790
|
|
|
1791
|
+
// src/mock/subjects.ts
|
|
1792
|
+
var MOCK_SUBJECT_KINDS = [
|
|
1793
|
+
{
|
|
1794
|
+
key: "vehicle",
|
|
1795
|
+
label: "Veh\xEDculo",
|
|
1796
|
+
label_plural: "Mi garage",
|
|
1797
|
+
max_per_account: 3,
|
|
1798
|
+
attach_to_order: true,
|
|
1799
|
+
fit_key_template: "{make}/{model}/{year}/{engine}",
|
|
1800
|
+
fields: [
|
|
1801
|
+
{ key: "make", label: "Marca", type: "select", source: "catalog", required: true },
|
|
1802
|
+
{ key: "model", label: "Modelo", type: "select", source: "catalog", depends_on: "make", required: true },
|
|
1803
|
+
{ key: "year", label: "A\xF1o", type: "year", source: "catalog", depends_on: "model", required: true },
|
|
1804
|
+
{ key: "engine", label: "Versi\xF3n / motor", type: "select", source: "catalog", depends_on: "year" },
|
|
1805
|
+
{ key: "plate", label: "Placas (opcional)", type: "text", max: 12, private: true }
|
|
1806
|
+
],
|
|
1807
|
+
catalog: {
|
|
1808
|
+
version: "mock-1",
|
|
1809
|
+
tree: {
|
|
1810
|
+
nissan: {
|
|
1811
|
+
label: "Nissan",
|
|
1812
|
+
children: {
|
|
1813
|
+
np300: {
|
|
1814
|
+
label: "NP300 Frontier",
|
|
1815
|
+
from: 2008,
|
|
1816
|
+
to: 2022,
|
|
1817
|
+
children: { "2.5": { label: "2.5 L", ranges: [[2010, 2015]] }, "2.4": { label: "2.4 L" } }
|
|
1818
|
+
},
|
|
1819
|
+
versa: { label: "Versa", from: 2012, to: 2024, children: { "1.6": { label: "1.6 L" } } }
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
ford: { label: "Ford", children: { f150: { label: "F-150", from: 2e3, to: 2020, children: { "5.0": { label: "5.0 L" } } } } }
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
key: "size",
|
|
1828
|
+
label: "Talla",
|
|
1829
|
+
label_plural: "Mis tallas",
|
|
1830
|
+
max_per_account: 5,
|
|
1831
|
+
fit_key_template: "{garment}/{value}",
|
|
1832
|
+
fields: [
|
|
1833
|
+
{ key: "garment", label: "Prenda", type: "text", required: true },
|
|
1834
|
+
{ key: "value", label: "Talla", type: "text", required: true, max: 8 }
|
|
1835
|
+
],
|
|
1836
|
+
catalog: null
|
|
1837
|
+
}
|
|
1838
|
+
];
|
|
1839
|
+
|
|
1686
1840
|
// src/mock/index.ts
|
|
1687
1841
|
var HttpError = class extends Error {
|
|
1688
1842
|
constructor(status, code, message, field, details, requiredScopes) {
|
|
@@ -1757,6 +1911,10 @@ function createMockStorefront(options = {}) {
|
|
|
1757
1911
|
online_verify: false
|
|
1758
1912
|
},
|
|
1759
1913
|
fiscal_profiles: { max: 10 },
|
|
1914
|
+
notifications: { channels: ["email"] },
|
|
1915
|
+
data_export: true,
|
|
1916
|
+
account_delete: true,
|
|
1917
|
+
...(options.subjectKinds?.length ?? 0) > 0 ? { subjects: { kinds: options.subjectKinds.map((k) => k.key), share_with_workshop: false } } : {},
|
|
1760
1918
|
...options.customerCapabilities
|
|
1761
1919
|
};
|
|
1762
1920
|
const missingScopes = options.missingScopes ?? [];
|
|
@@ -1788,6 +1946,7 @@ function createMockStorefront(options = {}) {
|
|
|
1788
1946
|
customers: buildCustomers(),
|
|
1789
1947
|
addresses: /* @__PURE__ */ new Map(),
|
|
1790
1948
|
fiscalProfiles: /* @__PURE__ */ new Map(),
|
|
1949
|
+
subjects: /* @__PURE__ */ new Map(),
|
|
1791
1950
|
orders: [
|
|
1792
1951
|
{
|
|
1793
1952
|
id: "ord_seed_1",
|
|
@@ -1808,6 +1967,10 @@ function createMockStorefront(options = {}) {
|
|
|
1808
1967
|
pendingRegistrations: /* @__PURE__ */ new Map(),
|
|
1809
1968
|
loginFailures: /* @__PURE__ */ new Map(),
|
|
1810
1969
|
sessions: /* @__PURE__ */ new Map(),
|
|
1970
|
+
sessionInfo: /* @__PURE__ */ new Map(),
|
|
1971
|
+
reauthFailures: /* @__PURE__ */ new Map(),
|
|
1972
|
+
exportsServed: /* @__PURE__ */ new Map(),
|
|
1973
|
+
deletedTokens: /* @__PURE__ */ new Set(),
|
|
1811
1974
|
idempotency: /* @__PURE__ */ new Map(),
|
|
1812
1975
|
checkouts: /* @__PURE__ */ new Map(),
|
|
1813
1976
|
payments: /* @__PURE__ */ new Map(),
|
|
@@ -1889,6 +2052,9 @@ function createMockStorefront(options = {}) {
|
|
|
1889
2052
|
if (list.length > 100) throw new HttpError(400, "validation", "At most 100 handles", "handles");
|
|
1890
2053
|
items = items.filter((p) => list.includes(p.handle));
|
|
1891
2054
|
}
|
|
2055
|
+
const tagsAny = (search.get("tags_any") ?? "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
2056
|
+
if (tagsAny.length > 5 || tagsAny.some((t) => t.length > 120)) throw new HttpError(400, "validation", "tags_any takes at most 5 tags of 120 characters", "tags_any");
|
|
2057
|
+
if (tagsAny.length > 0) items = items.filter((p) => p.tags.some((t) => tagsAny.includes(t.toLowerCase())));
|
|
1892
2058
|
const base = items;
|
|
1893
2059
|
if (search.get("available") === "true") items = items.filter((p) => p.available_for_sale);
|
|
1894
2060
|
const priceMin = search.get("price_min");
|
|
@@ -2016,6 +2182,7 @@ function createMockStorefront(options = {}) {
|
|
|
2016
2182
|
const expires = new Date(Date.now() + 24 * 36e5).toISOString();
|
|
2017
2183
|
return options.shippingRates.map((r) => ({ ...r, expires_at: r.expires_at ?? expires, cost: free ? fromCents(0, r.cost.currency_code) : r.cost }));
|
|
2018
2184
|
}
|
|
2185
|
+
if (free && options.shippingQuoteMode === "fixed_when_free") return [{ id: "flat", title: "Env\xEDo gratis", cost: fromCents(0, currency), eta: "2 a 5 d\xEDas h\xE1biles" }];
|
|
2019
2186
|
return [
|
|
2020
2187
|
{ id: "flat", title: free ? "Env\xEDo est\xE1ndar (gratis)" : "Env\xEDo est\xE1ndar", cost: fromCents(free ? 0 : flatShippingCents, currency), eta: "2 a 5 d\xEDas h\xE1biles" },
|
|
2021
2188
|
{ id: "express", title: "Env\xEDo expr\xE9s", cost: fromCents(flatShippingCents + 1e4, currency), eta: "1 a 2 d\xEDas h\xE1biles" }
|
|
@@ -2158,8 +2325,13 @@ function createMockStorefront(options = {}) {
|
|
|
2158
2325
|
}
|
|
2159
2326
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2160
2327
|
state.sessions.set(token, customer.id);
|
|
2328
|
+
trackSession(token, "password");
|
|
2161
2329
|
return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
|
|
2162
2330
|
}
|
|
2331
|
+
function trackSession(token, loginMethod) {
|
|
2332
|
+
const at = now();
|
|
2333
|
+
state.sessionInfo.set(token, { id: nextId("ses"), customer_id: state.sessions.get(token) ?? "", created_at: at, last_used_at: at, login_method: loginMethod, reauth_at: null });
|
|
2334
|
+
}
|
|
2163
2335
|
function validateAddress(body, partial = false) {
|
|
2164
2336
|
const required = ["address1", "city", "zip", "country_code"];
|
|
2165
2337
|
if (!partial) {
|
|
@@ -2314,8 +2486,10 @@ function createMockStorefront(options = {}) {
|
|
|
2314
2486
|
route("GET", "/carts/:id/delivery-options", "cart:write", ({ params }) => {
|
|
2315
2487
|
const cart = renderCart(getCartRecord(params[0]));
|
|
2316
2488
|
const net = toCents(cart.cost.subtotal.amount) - toCents(cart.cost.discount.amount);
|
|
2489
|
+
const rates = shippingRates(net);
|
|
2490
|
+
const free = options.shippingQuoteMode === "fixed_when_free" && rates.length > 0 && rates.every((r) => toCents(r.cost.amount) === 0);
|
|
2317
2491
|
return {
|
|
2318
|
-
body: { rates:
|
|
2492
|
+
body: { rates, free, fixed: free, pickup: state.shop.branches.filter((b) => b.pickup?.enabled !== false).map((b) => ({ branch_id: b.id, name: b.name, address: b.address, city: b.city })), partial: false, unavailable: [] },
|
|
2319
2493
|
cache: "private"
|
|
2320
2494
|
};
|
|
2321
2495
|
});
|
|
@@ -2587,6 +2761,7 @@ function createMockStorefront(options = {}) {
|
|
|
2587
2761
|
}
|
|
2588
2762
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2589
2763
|
state.sessions.set(token, customer.id);
|
|
2764
|
+
trackSession(token, kind);
|
|
2590
2765
|
return {
|
|
2591
2766
|
status: 201,
|
|
2592
2767
|
body: { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer },
|
|
@@ -2661,7 +2836,8 @@ function createMockStorefront(options = {}) {
|
|
|
2661
2836
|
state.loginFailures.delete(email);
|
|
2662
2837
|
return { status: 204 };
|
|
2663
2838
|
});
|
|
2664
|
-
const publicCustomer = (
|
|
2839
|
+
const publicCustomer = (c0) => {
|
|
2840
|
+
const c = { ...c0, locale: c0.locale ?? null, preferences: c0.preferences ?? { notifications: {} } };
|
|
2665
2841
|
if (!c.fiscal?.document) return c;
|
|
2666
2842
|
const { sha256: _sha, ...document } = c.fiscal.document;
|
|
2667
2843
|
return { ...c, fiscal: { ...c.fiscal, document } };
|
|
@@ -2673,6 +2849,8 @@ function createMockStorefront(options = {}) {
|
|
|
2673
2849
|
if ("first_name" in body) c.first_name = text(body.first_name);
|
|
2674
2850
|
if ("last_name" in body) c.last_name = text(body.last_name);
|
|
2675
2851
|
if ("phone" in body) c.phone = text(body.phone);
|
|
2852
|
+
if ("locale" in body) c.locale = parseLocale(body.locale);
|
|
2853
|
+
if ("preferences" in body) c.preferences = parsePreferences(c.preferences, body.preferences);
|
|
2676
2854
|
if ("fiscal" in body) {
|
|
2677
2855
|
if (body.fiscal === null) c.fiscal = null;
|
|
2678
2856
|
else {
|
|
@@ -2692,6 +2870,144 @@ function createMockStorefront(options = {}) {
|
|
|
2692
2870
|
}
|
|
2693
2871
|
return { body: publicCustomer(c), cache: "private" };
|
|
2694
2872
|
});
|
|
2873
|
+
const OPTIONAL_EVENTS = /* @__PURE__ */ new Set(["order_updates", "shipping", "restock"]);
|
|
2874
|
+
const MANDATORY_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "payment", "invoice", "security"]);
|
|
2875
|
+
const CHANNELS = /* @__PURE__ */ new Set(["email", "whatsapp"]);
|
|
2876
|
+
const REAUTH_WINDOW_MS = 5 * 6e4;
|
|
2877
|
+
const supportedLocales = () => [state.shop.locale, ...state.shop.locales].filter((l, i, a) => l && a.indexOf(l) === i);
|
|
2878
|
+
function parseLocale(raw) {
|
|
2879
|
+
if (raw === null || typeof raw === "string" && !raw.trim()) return null;
|
|
2880
|
+
if (typeof raw !== "string") throw new HttpError(400, "validation", "locale must be a string", "locale");
|
|
2881
|
+
const tag = raw.trim().replace("_", "-");
|
|
2882
|
+
const [lang = "", ...rest] = tag.split("-");
|
|
2883
|
+
const canonical = [lang.toLowerCase(), ...rest.map((p) => p.toUpperCase())].join("-");
|
|
2884
|
+
const supported = supportedLocales();
|
|
2885
|
+
const hit = supported.find((l) => l.toLowerCase() === canonical.toLowerCase()) ?? (rest.length === 0 ? supported.find((l) => l.split("-")[0].toLowerCase() === canonical) : void 0);
|
|
2886
|
+
if (!hit) throw new HttpError(400, "validation", "this store is not available in that language", "locale");
|
|
2887
|
+
return hit;
|
|
2888
|
+
}
|
|
2889
|
+
function parsePreferences(current, raw) {
|
|
2890
|
+
if (raw === null) return { notifications: {} };
|
|
2891
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "validation", "preferences must be an object", "preferences");
|
|
2892
|
+
const merged = structuredClone(current?.notifications ?? {});
|
|
2893
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2894
|
+
if (key !== "notifications") throw new HttpError(400, "validation", "unknown preference", `preferences.${key}`);
|
|
2895
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, "validation", "notifications must map each event to its channels", "preferences.notifications");
|
|
2896
|
+
for (const [event, channels] of Object.entries(value)) {
|
|
2897
|
+
const field = `preferences.notifications.${event}`;
|
|
2898
|
+
if (MANDATORY_EVENTS.has(event)) throw new HttpError(400, "validation", "this notification is required and cannot be turned off", field);
|
|
2899
|
+
if (!OPTIONAL_EVENTS.has(event)) throw new HttpError(400, "validation", "unknown notification", field);
|
|
2900
|
+
if (!channels || typeof channels !== "object" || Array.isArray(channels)) throw new HttpError(400, "validation", "each event maps its channels to true or false", field);
|
|
2901
|
+
for (const [channel, on] of Object.entries(channels)) {
|
|
2902
|
+
if (!CHANNELS.has(channel)) throw new HttpError(400, "validation", "unknown channel", `${field}.${channel}`);
|
|
2903
|
+
if (typeof on !== "boolean") throw new HttpError(400, "validation", "each channel must be true or false", `${field}.${channel}`);
|
|
2904
|
+
(merged[event] ??= {})[channel] = on;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
return { notifications: merged };
|
|
2909
|
+
}
|
|
2910
|
+
function sessionOf(headers) {
|
|
2911
|
+
const c = requireCustomer(headers);
|
|
2912
|
+
const token = headers.get("x-customer-token");
|
|
2913
|
+
if (!state.sessionInfo.has(token)) trackSession(token, "email");
|
|
2914
|
+
return { c, token, info: state.sessionInfo.get(token) };
|
|
2915
|
+
}
|
|
2916
|
+
function requireRecentReauth(info) {
|
|
2917
|
+
if (info.reauth_at === null || Date.now() - info.reauth_at > REAUTH_WINDOW_MS) {
|
|
2918
|
+
throw new HttpError(403, "reauth_required", "confirm your password or a code first (customer.reauth)");
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
route("GET", "/customer/sessions", "customer:read", ({ headers }) => {
|
|
2922
|
+
const { c, token } = sessionOf(headers);
|
|
2923
|
+
const nodes = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => ({ t, info: state.sessionInfo.get(t) })).sort((a, b) => b.info.last_used_at.localeCompare(a.info.last_used_at)).map(({ t, info }) => ({
|
|
2924
|
+
id: info.id,
|
|
2925
|
+
current: t === token,
|
|
2926
|
+
device_label: "Mock browser \xB7 Test",
|
|
2927
|
+
login_method: info.login_method,
|
|
2928
|
+
last_used_at: info.last_used_at,
|
|
2929
|
+
created_at: info.created_at
|
|
2930
|
+
}));
|
|
2931
|
+
return { body: { nodes }, cache: "private" };
|
|
2932
|
+
});
|
|
2933
|
+
route("DELETE", "/customer/sessions/:id", "customer:write", ({ headers, params }) => {
|
|
2934
|
+
const { c, token } = sessionOf(headers);
|
|
2935
|
+
const mine = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => t);
|
|
2936
|
+
if (params[0] === "others") {
|
|
2937
|
+
for (const t of mine) if (t !== token) state.sessions.delete(t);
|
|
2938
|
+
return { status: 204 };
|
|
2939
|
+
}
|
|
2940
|
+
const entry = [...state.sessionInfo.entries()].find(([, i]) => i.id === params[0]);
|
|
2941
|
+
if (!entry || entry[1].customer_id !== c.id) throw new HttpError(404, "not_found", "Session not found", "id");
|
|
2942
|
+
state.sessions.delete(entry[0]);
|
|
2943
|
+
return { status: 204 };
|
|
2944
|
+
});
|
|
2945
|
+
route("POST", "/customer/reauth", "customer:write", ({ headers, body }) => {
|
|
2946
|
+
const { c, info } = sessionOf(headers);
|
|
2947
|
+
const hasPassword = typeof body.password === "string" && body.password !== "";
|
|
2948
|
+
const hasCode = typeof body.otp_code === "string" && body.otp_code !== "";
|
|
2949
|
+
if (hasPassword === hasCode) throw new HttpError(400, "validation", "send either password or otp_code", "password");
|
|
2950
|
+
if ((state.reauthFailures.get(c.id) ?? 0) >= 5) throw new HttpError(429, "rate_limited", "Too many attempts, try again later");
|
|
2951
|
+
const field = hasPassword ? "password" : "otp_code";
|
|
2952
|
+
const ok = hasPassword ? c.email !== null && state.passwords.get(c.email) === body.password : [c.email && `email:${c.email}`, c.phone && `phone:${c.phone}`].some((id) => !!id && state.otps.get(id) === body.otp_code);
|
|
2953
|
+
if (!ok) {
|
|
2954
|
+
state.reauthFailures.set(c.id, (state.reauthFailures.get(c.id) ?? 0) + 1);
|
|
2955
|
+
throw new HttpError(400, "validation", "could not verify; check the credential and try again", field);
|
|
2956
|
+
}
|
|
2957
|
+
state.reauthFailures.delete(c.id);
|
|
2958
|
+
info.reauth_at = Date.now();
|
|
2959
|
+
return { body: { reauth_at: new Date(info.reauth_at).toISOString(), valid_for_seconds: REAUTH_WINDOW_MS / 1e3 }, cache: "private" };
|
|
2960
|
+
});
|
|
2961
|
+
route("POST", "/customer/data-export", "customer:write", ({ headers }) => {
|
|
2962
|
+
const { c, info } = sessionOf(headers);
|
|
2963
|
+
requireRecentReauth(info);
|
|
2964
|
+
if ((state.exportsServed.get(c.id) ?? 0) >= 3) throw new HttpError(429, "rate_limited", "Three exports a day; try again tomorrow");
|
|
2965
|
+
state.exportsServed.set(c.id, (state.exportsServed.get(c.id) ?? 0) + 1);
|
|
2966
|
+
const doc = {
|
|
2967
|
+
format: "storefront-customer-data/1",
|
|
2968
|
+
exported_at: now(),
|
|
2969
|
+
account: { id: c.id, email: c.email, phone: c.phone, first_name: c.first_name, last_name: c.last_name, locale: c.locale ?? null, created_at: c.created_at },
|
|
2970
|
+
preferences: c.preferences ?? { notifications: {} },
|
|
2971
|
+
addresses: state.addresses.get(c.id) ?? [],
|
|
2972
|
+
fiscal_profiles: state.fiscalProfiles.get(c.id) ?? [],
|
|
2973
|
+
orders: state.orders.filter((o) => o.customer_id === c.id).map((o) => ({ id: o.id, number: o.number, created_at: o.created_at, status: o.status, total: o.total }))
|
|
2974
|
+
};
|
|
2975
|
+
return {
|
|
2976
|
+
body: {
|
|
2977
|
+
filename: `account-data-${now().slice(0, 10)}.json`,
|
|
2978
|
+
content_type: "application/json",
|
|
2979
|
+
content_base64: btoa(unescape(encodeURIComponent(JSON.stringify(doc, null, 2))))
|
|
2980
|
+
},
|
|
2981
|
+
cache: "private"
|
|
2982
|
+
};
|
|
2983
|
+
});
|
|
2984
|
+
route("DELETE", "/customer", "customer:write", ({ headers, body }) => {
|
|
2985
|
+
const token = headers.get("x-customer-token");
|
|
2986
|
+
if (token && state.deletedTokens.has(token)) return { status: 204 };
|
|
2987
|
+
const { c, info } = sessionOf(headers);
|
|
2988
|
+
requireRecentReauth(info);
|
|
2989
|
+
if (typeof body.confirm !== "string" || !body.confirm.trim() || body.confirm.length > 100) {
|
|
2990
|
+
throw new HttpError(400, "validation", "confirm is required: the text the buyer typed", "confirm");
|
|
2991
|
+
}
|
|
2992
|
+
const closed = /* @__PURE__ */ new Set(["fulfilled", "delivered", "cancelled"]);
|
|
2993
|
+
if (state.orders.some((o) => o.customer_id === c.id && !closed.has(o.status))) {
|
|
2994
|
+
throw new HttpError(409, "conflict", "you have an order in progress; you can delete the account once it is delivered", "orders");
|
|
2995
|
+
}
|
|
2996
|
+
if (c.id === "cus_1" && state.pendingPayments.some((p) => p.status === "pending_payment")) {
|
|
2997
|
+
throw new HttpError(409, "conflict", "you have a payment waiting to be completed", "pending_payments");
|
|
2998
|
+
}
|
|
2999
|
+
for (const [t, id] of [...state.sessions.entries()]) {
|
|
3000
|
+
if (id === c.id) {
|
|
3001
|
+
state.sessions.delete(t);
|
|
3002
|
+
state.deletedTokens.add(t);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
if (c.email) state.passwords.delete(c.email);
|
|
3006
|
+
state.addresses.delete(c.id);
|
|
3007
|
+
state.fiscalProfiles.delete(c.id);
|
|
3008
|
+
state.customers = state.customers.filter((x) => x.id !== c.id);
|
|
3009
|
+
return { status: 204 };
|
|
3010
|
+
});
|
|
2695
3011
|
const orderView = ({ customer_id: _c, ...o }, detail) => {
|
|
2696
3012
|
const order = { ...o, invoice: orderInvoice(o) };
|
|
2697
3013
|
if (order.shipments) order.shipments = order.shipments.map((s) => isTestShipment(s) ? { ...s, tracking_number: null, tracking_url: null } : { ...s, events: sortTrackingEvents(s.events) });
|
|
@@ -2848,6 +3164,89 @@ function createMockStorefront(options = {}) {
|
|
|
2848
3164
|
mirrorDefaultProfile(c);
|
|
2849
3165
|
return { status: 204 };
|
|
2850
3166
|
});
|
|
3167
|
+
const MAX_SUBJECTS = 50;
|
|
3168
|
+
const subjectsOf = (c) => state.subjects.get(c.id) ?? [];
|
|
3169
|
+
const kinds = () => options.subjectKinds ?? [];
|
|
3170
|
+
const publicSubject = (s) => {
|
|
3171
|
+
const { private: priv, ...rest } = s;
|
|
3172
|
+
return { ...rest, attributes: { ...s.attributes, ...Object.fromEntries(Object.keys(priv).map((k) => [k, PRIVATE_MASK + (priv[k].length > 3 ? priv[k].slice(-2) : "")])) } };
|
|
3173
|
+
};
|
|
3174
|
+
const ownSubject = (c, id) => {
|
|
3175
|
+
const s = subjectsOf(c).find((x) => x.id === id);
|
|
3176
|
+
if (!s) throw new HttpError(404, "not_found", "Item not found", "id");
|
|
3177
|
+
return s;
|
|
3178
|
+
};
|
|
3179
|
+
const setDefaultSubject = (c, id) => {
|
|
3180
|
+
const kind = ownSubject(c, id).kind;
|
|
3181
|
+
state.subjects.set(c.id, subjectsOf(c).map((s) => s.kind !== kind ? s : s.id === id ? { ...s, is_default: true, last_used_at: now() } : { ...s, is_default: false }));
|
|
3182
|
+
};
|
|
3183
|
+
const resolveOrThrow = (kind, values) => {
|
|
3184
|
+
const r = resolveSubject(kind, values);
|
|
3185
|
+
if (!r.ok) {
|
|
3186
|
+
const [field, code] = Object.entries(r.problems)[0];
|
|
3187
|
+
throw new HttpError(422, "validation", `${field}: ${code}`, field === "_" ? "attributes" : `attributes.${field}`);
|
|
3188
|
+
}
|
|
3189
|
+
return r.subject;
|
|
3190
|
+
};
|
|
3191
|
+
const cleanNickname = (v) => {
|
|
3192
|
+
if (v === void 0 || v === null || v === "") return null;
|
|
3193
|
+
if (typeof v !== "string" || [...v.trim()].length > 40) throw new HttpError(422, "validation", "nickname is too long (max 40)", "nickname");
|
|
3194
|
+
return v.trim().split(/\s+/).join(" ");
|
|
3195
|
+
};
|
|
3196
|
+
route("GET", "/subject-kinds", "content:read", () => ({ body: { nodes: kinds() }, cache: "public" }));
|
|
3197
|
+
route("GET", "/customer/subjects", "customer:read", ({ headers, search }) => {
|
|
3198
|
+
const c = requireCustomer(headers);
|
|
3199
|
+
const kind = search.get("kind");
|
|
3200
|
+
const nodes = subjectsOf(c).filter((s) => !kind || s.kind === kind).sort((a, b) => a.kind.localeCompare(b.kind) || Number(b.is_default) - Number(a.is_default) || (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""));
|
|
3201
|
+
return { body: { nodes: nodes.map(publicSubject) }, cache: "private" };
|
|
3202
|
+
});
|
|
3203
|
+
route("POST", "/customer/subjects", "customer:write", ({ headers, body }) => {
|
|
3204
|
+
const c = requireCustomer(headers);
|
|
3205
|
+
const kind = kinds().find((k) => k.key === body.kind);
|
|
3206
|
+
if (!kind) throw new HttpError(422, "validation", "This store has no such kind", "kind");
|
|
3207
|
+
const attrs = body.attributes;
|
|
3208
|
+
if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) throw new HttpError(422, "validation", "attributes must be an object", "attributes");
|
|
3209
|
+
const r = resolveOrThrow(kind, attrs);
|
|
3210
|
+
const list = subjectsOf(c);
|
|
3211
|
+
if (list.length >= MAX_SUBJECTS || list.filter((s) => s.kind === kind.key).length >= kind.max_per_account) throw new HttpError(409, "conflict", "Limit reached; delete one first", "kind");
|
|
3212
|
+
if (r.fit_key && list.some((s) => s.kind === kind.key && s.fit_key === r.fit_key)) throw new HttpError(409, "conflict", "You already saved this item", "attributes");
|
|
3213
|
+
const subject = { id: nextId("subj"), kind: kind.key, label: r.label, nickname: cleanNickname(body.nickname), fit_key: r.fit_key, attributes: r.attributes, private: r.private, is_default: false, last_used_at: null, created_at: now() };
|
|
3214
|
+
state.subjects.set(c.id, [...list, subject]);
|
|
3215
|
+
if (!list.some((s) => s.kind === kind.key) || body.is_default === true) setDefaultSubject(c, subject.id);
|
|
3216
|
+
return { status: 201, body: publicSubject(ownSubject(c, subject.id)), cache: "private" };
|
|
3217
|
+
});
|
|
3218
|
+
route("PATCH", "/customer/subjects/:id", "customer:write", ({ headers, params, body }) => {
|
|
3219
|
+
const c = requireCustomer(headers);
|
|
3220
|
+
const cur = ownSubject(c, params[0]);
|
|
3221
|
+
if (body.is_default === false && cur.is_default) throw new HttpError(422, "validation", "Make another item the default instead", "is_default");
|
|
3222
|
+
let next = { ...cur };
|
|
3223
|
+
if ("nickname" in body) next.nickname = cleanNickname(body.nickname);
|
|
3224
|
+
if ("attributes" in body) {
|
|
3225
|
+
const kind = kinds().find((k) => k.key === cur.kind);
|
|
3226
|
+
if (!kind) throw new HttpError(422, "validation", "This store no longer offers this kind", "kind");
|
|
3227
|
+
const sent = body.attributes ?? {};
|
|
3228
|
+
const merged = { ...cur.attributes, ...cur.private };
|
|
3229
|
+
for (const [k, v] of Object.entries(sent)) {
|
|
3230
|
+
if (k in cur.private && isMasked(v)) continue;
|
|
3231
|
+
if (v === null) delete merged[k];
|
|
3232
|
+
else merged[k] = v;
|
|
3233
|
+
}
|
|
3234
|
+
const r = resolveOrThrow(kind, merged);
|
|
3235
|
+
if (r.fit_key && subjectsOf(c).some((s) => s.id !== cur.id && s.kind === cur.kind && s.fit_key === r.fit_key)) throw new HttpError(409, "conflict", "You already saved this item", "attributes");
|
|
3236
|
+
next = { ...next, label: r.label, fit_key: r.fit_key, attributes: r.attributes, private: r.private };
|
|
3237
|
+
}
|
|
3238
|
+
state.subjects.set(c.id, subjectsOf(c).map((s) => s.id === cur.id ? next : s));
|
|
3239
|
+
if (body.is_default === true && !cur.is_default) setDefaultSubject(c, cur.id);
|
|
3240
|
+
return { body: publicSubject(ownSubject(c, cur.id)), cache: "private" };
|
|
3241
|
+
});
|
|
3242
|
+
route("DELETE", "/customer/subjects/:id", "customer:write", ({ headers, params }) => {
|
|
3243
|
+
const c = requireCustomer(headers);
|
|
3244
|
+
const cur = ownSubject(c, params[0]);
|
|
3245
|
+
state.subjects.set(c.id, subjectsOf(c).filter((s) => s.id !== cur.id));
|
|
3246
|
+
const heir = subjectsOf(c).filter((s) => s.kind === cur.kind).sort((a, b) => (b.last_used_at ?? "").localeCompare(a.last_used_at ?? ""))[0];
|
|
3247
|
+
if (cur.is_default && heir) setDefaultSubject(c, heir.id);
|
|
3248
|
+
return { status: 204 };
|
|
3249
|
+
});
|
|
2851
3250
|
route("GET", "/customer/addresses", "customer:read", ({ headers }) => {
|
|
2852
3251
|
const c = requireCustomer(headers);
|
|
2853
3252
|
return { body: { nodes: state.addresses.get(c.id) ?? [] }, cache: "private" };
|
|
@@ -2944,7 +3343,7 @@ function createMockStorefront(options = {}) {
|
|
|
2944
3343
|
return errorResponse(new HttpError(403, "insufficient_scope", `Token lacks scope ${r.scope}`, void 0, void 0, [r.scope]));
|
|
2945
3344
|
}
|
|
2946
3345
|
let body = {};
|
|
2947
|
-
if (method !== "GET"
|
|
3346
|
+
if (method !== "GET") {
|
|
2948
3347
|
const text = await request.text();
|
|
2949
3348
|
if (text) {
|
|
2950
3349
|
try {
|
|
@@ -3005,6 +3404,7 @@ exports.MOCK_IDENTITY_DOCUMENT_MX_CSF = MOCK_IDENTITY_DOCUMENT_MX_CSF;
|
|
|
3005
3404
|
exports.MOCK_IMAGE_URL = MOCK_IMAGE_URL;
|
|
3006
3405
|
exports.MOCK_POSTAL_CENTROIDS = MOCK_POSTAL_CENTROIDS;
|
|
3007
3406
|
exports.MOCK_REGION_MX = MOCK_REGION_MX;
|
|
3407
|
+
exports.MOCK_SUBJECT_KINDS = MOCK_SUBJECT_KINDS;
|
|
3008
3408
|
exports.createMockStorefront = createMockStorefront;
|
|
3009
3409
|
//# sourceMappingURL=mock.cjs.map
|
|
3010
3410
|
//# sourceMappingURL=mock.cjs.map
|