@asteby/storefront 0.16.1 → 0.17.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/{types-DSHzYSHU.d.ts → account-settings-DNcJKOQl.d.ts} +144 -1
- package/dist/{types-C5nEQicj.d.cts → account-settings-H6zuI7iG.d.cts} +144 -1
- package/dist/cli.js +38 -1
- package/dist/fiscal-document.d.cts +2 -2
- package/dist/fiscal-document.d.ts +2 -2
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -70
- package/dist/index.d.ts +34 -70
- package/dist/index.js +61 -1
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +156 -2
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +16 -1
- package/dist/mock.d.ts +16 -1
- package/dist/mock.js +156 -2
- 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/mock.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a6 as IdentityRulesV1, bg as RegionFiscalId, a4 as IdentityDocumentSpec, be as Region, bx as Shop, a$ as Product, y as Collection, bG as Theme, aG as Page, ap as Menu, c as Article, C as Cart, a as Address, af as InvoiceRequest, B as Customer, $ as FiscalProfile, aw as Order, aJ as PaymentAttemptResult, aY as PendingPayment, ah as InvoiceStage, D as CustomerCapabilities, b6 as PromotionAnnouncement, bw as ShippingRate, aQ as PaymentMethod } from './account-settings-H6zuI7iG.cjs';
|
|
2
2
|
import './geo.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -143,6 +143,21 @@ type MockState = {
|
|
|
143
143
|
/** Consecutive failed password logins by email. */
|
|
144
144
|
loginFailures: Map<string, number>;
|
|
145
145
|
sessions: Map<string, string>;
|
|
146
|
+
/** What «Devices» shows for each session token (contract 1.9). */
|
|
147
|
+
sessionInfo: Map<string, {
|
|
148
|
+
id: string;
|
|
149
|
+
customer_id: string;
|
|
150
|
+
created_at: string;
|
|
151
|
+
last_used_at: string;
|
|
152
|
+
login_method: string;
|
|
153
|
+
reauth_at: number | null;
|
|
154
|
+
}>;
|
|
155
|
+
/** Wrong re-authentications per customer (5 and the mock answers 429). */
|
|
156
|
+
reauthFailures: Map<string, number>;
|
|
157
|
+
/** Data exports served per customer (3 a day). */
|
|
158
|
+
exportsServed: Map<string, number>;
|
|
159
|
+
/** Tokens of accounts that were deleted: deleting again answers 204, as the server does. */
|
|
160
|
+
deletedTokens: Set<string>;
|
|
146
161
|
idempotency: Map<string, {
|
|
147
162
|
status: number;
|
|
148
163
|
body: unknown;
|
package/dist/mock.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a6 as IdentityRulesV1, bg as RegionFiscalId, a4 as IdentityDocumentSpec, be as Region, bx as Shop, a$ as Product, y as Collection, bG as Theme, aG as Page, ap as Menu, c as Article, C as Cart, a as Address, af as InvoiceRequest, B as Customer, $ as FiscalProfile, aw as Order, aJ as PaymentAttemptResult, aY as PendingPayment, ah as InvoiceStage, D as CustomerCapabilities, b6 as PromotionAnnouncement, bw as ShippingRate, aQ as PaymentMethod } from './account-settings-DNcJKOQl.js';
|
|
2
2
|
import './geo.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -143,6 +143,21 @@ type MockState = {
|
|
|
143
143
|
/** Consecutive failed password logins by email. */
|
|
144
144
|
loginFailures: Map<string, number>;
|
|
145
145
|
sessions: Map<string, string>;
|
|
146
|
+
/** What «Devices» shows for each session token (contract 1.9). */
|
|
147
|
+
sessionInfo: Map<string, {
|
|
148
|
+
id: string;
|
|
149
|
+
customer_id: string;
|
|
150
|
+
created_at: string;
|
|
151
|
+
last_used_at: string;
|
|
152
|
+
login_method: string;
|
|
153
|
+
reauth_at: number | null;
|
|
154
|
+
}>;
|
|
155
|
+
/** Wrong re-authentications per customer (5 and the mock answers 429). */
|
|
156
|
+
reauthFailures: Map<string, number>;
|
|
157
|
+
/** Data exports served per customer (3 a day). */
|
|
158
|
+
exportsServed: Map<string, number>;
|
|
159
|
+
/** Tokens of accounts that were deleted: deleting again answers 204, as the server does. */
|
|
160
|
+
deletedTokens: Set<string>;
|
|
146
161
|
idempotency: Map<string, {
|
|
147
162
|
status: number;
|
|
148
163
|
body: unknown;
|
package/dist/mock.js
CHANGED
|
@@ -1585,6 +1585,9 @@ function createMockStorefront(options = {}) {
|
|
|
1585
1585
|
online_verify: false
|
|
1586
1586
|
},
|
|
1587
1587
|
fiscal_profiles: { max: 10 },
|
|
1588
|
+
notifications: { channels: ["email"] },
|
|
1589
|
+
data_export: true,
|
|
1590
|
+
account_delete: true,
|
|
1588
1591
|
...options.customerCapabilities
|
|
1589
1592
|
};
|
|
1590
1593
|
const missingScopes = options.missingScopes ?? [];
|
|
@@ -1636,6 +1639,10 @@ function createMockStorefront(options = {}) {
|
|
|
1636
1639
|
pendingRegistrations: /* @__PURE__ */ new Map(),
|
|
1637
1640
|
loginFailures: /* @__PURE__ */ new Map(),
|
|
1638
1641
|
sessions: /* @__PURE__ */ new Map(),
|
|
1642
|
+
sessionInfo: /* @__PURE__ */ new Map(),
|
|
1643
|
+
reauthFailures: /* @__PURE__ */ new Map(),
|
|
1644
|
+
exportsServed: /* @__PURE__ */ new Map(),
|
|
1645
|
+
deletedTokens: /* @__PURE__ */ new Set(),
|
|
1639
1646
|
idempotency: /* @__PURE__ */ new Map(),
|
|
1640
1647
|
checkouts: /* @__PURE__ */ new Map(),
|
|
1641
1648
|
payments: /* @__PURE__ */ new Map(),
|
|
@@ -1986,8 +1993,13 @@ function createMockStorefront(options = {}) {
|
|
|
1986
1993
|
}
|
|
1987
1994
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
1988
1995
|
state.sessions.set(token, customer.id);
|
|
1996
|
+
trackSession(token, "password");
|
|
1989
1997
|
return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
|
|
1990
1998
|
}
|
|
1999
|
+
function trackSession(token, loginMethod) {
|
|
2000
|
+
const at = now();
|
|
2001
|
+
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 });
|
|
2002
|
+
}
|
|
1991
2003
|
function validateAddress(body, partial = false) {
|
|
1992
2004
|
const required = ["address1", "city", "zip", "country_code"];
|
|
1993
2005
|
if (!partial) {
|
|
@@ -2415,6 +2427,7 @@ function createMockStorefront(options = {}) {
|
|
|
2415
2427
|
}
|
|
2416
2428
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2417
2429
|
state.sessions.set(token, customer.id);
|
|
2430
|
+
trackSession(token, kind);
|
|
2418
2431
|
return {
|
|
2419
2432
|
status: 201,
|
|
2420
2433
|
body: { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer },
|
|
@@ -2489,7 +2502,8 @@ function createMockStorefront(options = {}) {
|
|
|
2489
2502
|
state.loginFailures.delete(email);
|
|
2490
2503
|
return { status: 204 };
|
|
2491
2504
|
});
|
|
2492
|
-
const publicCustomer = (
|
|
2505
|
+
const publicCustomer = (c0) => {
|
|
2506
|
+
const c = { ...c0, locale: c0.locale ?? null, preferences: c0.preferences ?? { notifications: {} } };
|
|
2493
2507
|
if (!c.fiscal?.document) return c;
|
|
2494
2508
|
const { sha256: _sha, ...document } = c.fiscal.document;
|
|
2495
2509
|
return { ...c, fiscal: { ...c.fiscal, document } };
|
|
@@ -2501,6 +2515,8 @@ function createMockStorefront(options = {}) {
|
|
|
2501
2515
|
if ("first_name" in body) c.first_name = text(body.first_name);
|
|
2502
2516
|
if ("last_name" in body) c.last_name = text(body.last_name);
|
|
2503
2517
|
if ("phone" in body) c.phone = text(body.phone);
|
|
2518
|
+
if ("locale" in body) c.locale = parseLocale(body.locale);
|
|
2519
|
+
if ("preferences" in body) c.preferences = parsePreferences(c.preferences, body.preferences);
|
|
2504
2520
|
if ("fiscal" in body) {
|
|
2505
2521
|
if (body.fiscal === null) c.fiscal = null;
|
|
2506
2522
|
else {
|
|
@@ -2520,6 +2536,144 @@ function createMockStorefront(options = {}) {
|
|
|
2520
2536
|
}
|
|
2521
2537
|
return { body: publicCustomer(c), cache: "private" };
|
|
2522
2538
|
});
|
|
2539
|
+
const OPTIONAL_EVENTS = /* @__PURE__ */ new Set(["order_updates", "shipping", "restock"]);
|
|
2540
|
+
const MANDATORY_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "payment", "invoice", "security"]);
|
|
2541
|
+
const CHANNELS = /* @__PURE__ */ new Set(["email", "whatsapp"]);
|
|
2542
|
+
const REAUTH_WINDOW_MS = 5 * 6e4;
|
|
2543
|
+
const supportedLocales = () => [state.shop.locale, ...state.shop.locales].filter((l, i, a) => l && a.indexOf(l) === i);
|
|
2544
|
+
function parseLocale(raw) {
|
|
2545
|
+
if (raw === null || typeof raw === "string" && !raw.trim()) return null;
|
|
2546
|
+
if (typeof raw !== "string") throw new HttpError(400, "validation", "locale must be a string", "locale");
|
|
2547
|
+
const tag = raw.trim().replace("_", "-");
|
|
2548
|
+
const [lang = "", ...rest] = tag.split("-");
|
|
2549
|
+
const canonical = [lang.toLowerCase(), ...rest.map((p) => p.toUpperCase())].join("-");
|
|
2550
|
+
const supported = supportedLocales();
|
|
2551
|
+
const hit = supported.find((l) => l.toLowerCase() === canonical.toLowerCase()) ?? (rest.length === 0 ? supported.find((l) => l.split("-")[0].toLowerCase() === canonical) : void 0);
|
|
2552
|
+
if (!hit) throw new HttpError(400, "validation", "this store is not available in that language", "locale");
|
|
2553
|
+
return hit;
|
|
2554
|
+
}
|
|
2555
|
+
function parsePreferences(current, raw) {
|
|
2556
|
+
if (raw === null) return { notifications: {} };
|
|
2557
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "validation", "preferences must be an object", "preferences");
|
|
2558
|
+
const merged = structuredClone(current?.notifications ?? {});
|
|
2559
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2560
|
+
if (key !== "notifications") throw new HttpError(400, "validation", "unknown preference", `preferences.${key}`);
|
|
2561
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, "validation", "notifications must map each event to its channels", "preferences.notifications");
|
|
2562
|
+
for (const [event, channels] of Object.entries(value)) {
|
|
2563
|
+
const field = `preferences.notifications.${event}`;
|
|
2564
|
+
if (MANDATORY_EVENTS.has(event)) throw new HttpError(400, "validation", "this notification is required and cannot be turned off", field);
|
|
2565
|
+
if (!OPTIONAL_EVENTS.has(event)) throw new HttpError(400, "validation", "unknown notification", field);
|
|
2566
|
+
if (!channels || typeof channels !== "object" || Array.isArray(channels)) throw new HttpError(400, "validation", "each event maps its channels to true or false", field);
|
|
2567
|
+
for (const [channel, on] of Object.entries(channels)) {
|
|
2568
|
+
if (!CHANNELS.has(channel)) throw new HttpError(400, "validation", "unknown channel", `${field}.${channel}`);
|
|
2569
|
+
if (typeof on !== "boolean") throw new HttpError(400, "validation", "each channel must be true or false", `${field}.${channel}`);
|
|
2570
|
+
(merged[event] ??= {})[channel] = on;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return { notifications: merged };
|
|
2575
|
+
}
|
|
2576
|
+
function sessionOf(headers) {
|
|
2577
|
+
const c = requireCustomer(headers);
|
|
2578
|
+
const token = headers.get("x-customer-token");
|
|
2579
|
+
if (!state.sessionInfo.has(token)) trackSession(token, "email");
|
|
2580
|
+
return { c, token, info: state.sessionInfo.get(token) };
|
|
2581
|
+
}
|
|
2582
|
+
function requireRecentReauth(info) {
|
|
2583
|
+
if (info.reauth_at === null || Date.now() - info.reauth_at > REAUTH_WINDOW_MS) {
|
|
2584
|
+
throw new HttpError(403, "reauth_required", "confirm your password or a code first (customer.reauth)");
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
route("GET", "/customer/sessions", "customer:read", ({ headers }) => {
|
|
2588
|
+
const { c, token } = sessionOf(headers);
|
|
2589
|
+
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 }) => ({
|
|
2590
|
+
id: info.id,
|
|
2591
|
+
current: t === token,
|
|
2592
|
+
device_label: "Mock browser \xB7 Test",
|
|
2593
|
+
login_method: info.login_method,
|
|
2594
|
+
last_used_at: info.last_used_at,
|
|
2595
|
+
created_at: info.created_at
|
|
2596
|
+
}));
|
|
2597
|
+
return { body: { nodes }, cache: "private" };
|
|
2598
|
+
});
|
|
2599
|
+
route("DELETE", "/customer/sessions/:id", "customer:write", ({ headers, params }) => {
|
|
2600
|
+
const { c, token } = sessionOf(headers);
|
|
2601
|
+
const mine = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => t);
|
|
2602
|
+
if (params[0] === "others") {
|
|
2603
|
+
for (const t of mine) if (t !== token) state.sessions.delete(t);
|
|
2604
|
+
return { status: 204 };
|
|
2605
|
+
}
|
|
2606
|
+
const entry = [...state.sessionInfo.entries()].find(([, i]) => i.id === params[0]);
|
|
2607
|
+
if (!entry || entry[1].customer_id !== c.id) throw new HttpError(404, "not_found", "Session not found", "id");
|
|
2608
|
+
state.sessions.delete(entry[0]);
|
|
2609
|
+
return { status: 204 };
|
|
2610
|
+
});
|
|
2611
|
+
route("POST", "/customer/reauth", "customer:write", ({ headers, body }) => {
|
|
2612
|
+
const { c, info } = sessionOf(headers);
|
|
2613
|
+
const hasPassword = typeof body.password === "string" && body.password !== "";
|
|
2614
|
+
const hasCode = typeof body.otp_code === "string" && body.otp_code !== "";
|
|
2615
|
+
if (hasPassword === hasCode) throw new HttpError(400, "validation", "send either password or otp_code", "password");
|
|
2616
|
+
if ((state.reauthFailures.get(c.id) ?? 0) >= 5) throw new HttpError(429, "rate_limited", "Too many attempts, try again later");
|
|
2617
|
+
const field = hasPassword ? "password" : "otp_code";
|
|
2618
|
+
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);
|
|
2619
|
+
if (!ok) {
|
|
2620
|
+
state.reauthFailures.set(c.id, (state.reauthFailures.get(c.id) ?? 0) + 1);
|
|
2621
|
+
throw new HttpError(400, "validation", "could not verify; check the credential and try again", field);
|
|
2622
|
+
}
|
|
2623
|
+
state.reauthFailures.delete(c.id);
|
|
2624
|
+
info.reauth_at = Date.now();
|
|
2625
|
+
return { body: { reauth_at: new Date(info.reauth_at).toISOString(), valid_for_seconds: REAUTH_WINDOW_MS / 1e3 }, cache: "private" };
|
|
2626
|
+
});
|
|
2627
|
+
route("POST", "/customer/data-export", "customer:write", ({ headers }) => {
|
|
2628
|
+
const { c, info } = sessionOf(headers);
|
|
2629
|
+
requireRecentReauth(info);
|
|
2630
|
+
if ((state.exportsServed.get(c.id) ?? 0) >= 3) throw new HttpError(429, "rate_limited", "Three exports a day; try again tomorrow");
|
|
2631
|
+
state.exportsServed.set(c.id, (state.exportsServed.get(c.id) ?? 0) + 1);
|
|
2632
|
+
const doc = {
|
|
2633
|
+
format: "storefront-customer-data/1",
|
|
2634
|
+
exported_at: now(),
|
|
2635
|
+
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 },
|
|
2636
|
+
preferences: c.preferences ?? { notifications: {} },
|
|
2637
|
+
addresses: state.addresses.get(c.id) ?? [],
|
|
2638
|
+
fiscal_profiles: state.fiscalProfiles.get(c.id) ?? [],
|
|
2639
|
+
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 }))
|
|
2640
|
+
};
|
|
2641
|
+
return {
|
|
2642
|
+
body: {
|
|
2643
|
+
filename: `account-data-${now().slice(0, 10)}.json`,
|
|
2644
|
+
content_type: "application/json",
|
|
2645
|
+
content_base64: btoa(unescape(encodeURIComponent(JSON.stringify(doc, null, 2))))
|
|
2646
|
+
},
|
|
2647
|
+
cache: "private"
|
|
2648
|
+
};
|
|
2649
|
+
});
|
|
2650
|
+
route("DELETE", "/customer", "customer:write", ({ headers, body }) => {
|
|
2651
|
+
const token = headers.get("x-customer-token");
|
|
2652
|
+
if (token && state.deletedTokens.has(token)) return { status: 204 };
|
|
2653
|
+
const { c, info } = sessionOf(headers);
|
|
2654
|
+
requireRecentReauth(info);
|
|
2655
|
+
if (typeof body.confirm !== "string" || !body.confirm.trim() || body.confirm.length > 100) {
|
|
2656
|
+
throw new HttpError(400, "validation", "confirm is required: the text the buyer typed", "confirm");
|
|
2657
|
+
}
|
|
2658
|
+
const closed = /* @__PURE__ */ new Set(["fulfilled", "delivered", "cancelled"]);
|
|
2659
|
+
if (state.orders.some((o) => o.customer_id === c.id && !closed.has(o.status))) {
|
|
2660
|
+
throw new HttpError(409, "conflict", "you have an order in progress; you can delete the account once it is delivered", "orders");
|
|
2661
|
+
}
|
|
2662
|
+
if (c.id === "cus_1" && state.pendingPayments.some((p) => p.status === "pending_payment")) {
|
|
2663
|
+
throw new HttpError(409, "conflict", "you have a payment waiting to be completed", "pending_payments");
|
|
2664
|
+
}
|
|
2665
|
+
for (const [t, id] of [...state.sessions.entries()]) {
|
|
2666
|
+
if (id === c.id) {
|
|
2667
|
+
state.sessions.delete(t);
|
|
2668
|
+
state.deletedTokens.add(t);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (c.email) state.passwords.delete(c.email);
|
|
2672
|
+
state.addresses.delete(c.id);
|
|
2673
|
+
state.fiscalProfiles.delete(c.id);
|
|
2674
|
+
state.customers = state.customers.filter((x) => x.id !== c.id);
|
|
2675
|
+
return { status: 204 };
|
|
2676
|
+
});
|
|
2523
2677
|
const orderView = ({ customer_id: _c, ...o }, detail) => {
|
|
2524
2678
|
const order = { ...o, invoice: orderInvoice(o) };
|
|
2525
2679
|
if (order.shipments) order.shipments = order.shipments.map((s) => isTestShipment(s) ? { ...s, tracking_number: null, tracking_url: null } : { ...s, events: sortTrackingEvents(s.events) });
|
|
@@ -2772,7 +2926,7 @@ function createMockStorefront(options = {}) {
|
|
|
2772
2926
|
return errorResponse(new HttpError(403, "insufficient_scope", `Token lacks scope ${r.scope}`, void 0, void 0, [r.scope]));
|
|
2773
2927
|
}
|
|
2774
2928
|
let body = {};
|
|
2775
|
-
if (method !== "GET"
|
|
2929
|
+
if (method !== "GET") {
|
|
2776
2930
|
const text = await request.text();
|
|
2777
2931
|
if (text) {
|
|
2778
2932
|
try {
|