@asteby/storefront 0.16.0 → 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-DFK20t_g.d.ts → account-settings-DNcJKOQl.d.ts} +150 -1
- package/dist/{types-TQPpYbac.d.cts → account-settings-H6zuI7iG.d.cts} +150 -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.cjs
CHANGED
|
@@ -1757,6 +1757,9 @@ function createMockStorefront(options = {}) {
|
|
|
1757
1757
|
online_verify: false
|
|
1758
1758
|
},
|
|
1759
1759
|
fiscal_profiles: { max: 10 },
|
|
1760
|
+
notifications: { channels: ["email"] },
|
|
1761
|
+
data_export: true,
|
|
1762
|
+
account_delete: true,
|
|
1760
1763
|
...options.customerCapabilities
|
|
1761
1764
|
};
|
|
1762
1765
|
const missingScopes = options.missingScopes ?? [];
|
|
@@ -1808,6 +1811,10 @@ function createMockStorefront(options = {}) {
|
|
|
1808
1811
|
pendingRegistrations: /* @__PURE__ */ new Map(),
|
|
1809
1812
|
loginFailures: /* @__PURE__ */ new Map(),
|
|
1810
1813
|
sessions: /* @__PURE__ */ new Map(),
|
|
1814
|
+
sessionInfo: /* @__PURE__ */ new Map(),
|
|
1815
|
+
reauthFailures: /* @__PURE__ */ new Map(),
|
|
1816
|
+
exportsServed: /* @__PURE__ */ new Map(),
|
|
1817
|
+
deletedTokens: /* @__PURE__ */ new Set(),
|
|
1811
1818
|
idempotency: /* @__PURE__ */ new Map(),
|
|
1812
1819
|
checkouts: /* @__PURE__ */ new Map(),
|
|
1813
1820
|
payments: /* @__PURE__ */ new Map(),
|
|
@@ -2158,8 +2165,13 @@ function createMockStorefront(options = {}) {
|
|
|
2158
2165
|
}
|
|
2159
2166
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2160
2167
|
state.sessions.set(token, customer.id);
|
|
2168
|
+
trackSession(token, "password");
|
|
2161
2169
|
return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
|
|
2162
2170
|
}
|
|
2171
|
+
function trackSession(token, loginMethod) {
|
|
2172
|
+
const at = now();
|
|
2173
|
+
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 });
|
|
2174
|
+
}
|
|
2163
2175
|
function validateAddress(body, partial = false) {
|
|
2164
2176
|
const required = ["address1", "city", "zip", "country_code"];
|
|
2165
2177
|
if (!partial) {
|
|
@@ -2587,6 +2599,7 @@ function createMockStorefront(options = {}) {
|
|
|
2587
2599
|
}
|
|
2588
2600
|
const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
|
|
2589
2601
|
state.sessions.set(token, customer.id);
|
|
2602
|
+
trackSession(token, kind);
|
|
2590
2603
|
return {
|
|
2591
2604
|
status: 201,
|
|
2592
2605
|
body: { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer },
|
|
@@ -2661,7 +2674,8 @@ function createMockStorefront(options = {}) {
|
|
|
2661
2674
|
state.loginFailures.delete(email);
|
|
2662
2675
|
return { status: 204 };
|
|
2663
2676
|
});
|
|
2664
|
-
const publicCustomer = (
|
|
2677
|
+
const publicCustomer = (c0) => {
|
|
2678
|
+
const c = { ...c0, locale: c0.locale ?? null, preferences: c0.preferences ?? { notifications: {} } };
|
|
2665
2679
|
if (!c.fiscal?.document) return c;
|
|
2666
2680
|
const { sha256: _sha, ...document } = c.fiscal.document;
|
|
2667
2681
|
return { ...c, fiscal: { ...c.fiscal, document } };
|
|
@@ -2673,6 +2687,8 @@ function createMockStorefront(options = {}) {
|
|
|
2673
2687
|
if ("first_name" in body) c.first_name = text(body.first_name);
|
|
2674
2688
|
if ("last_name" in body) c.last_name = text(body.last_name);
|
|
2675
2689
|
if ("phone" in body) c.phone = text(body.phone);
|
|
2690
|
+
if ("locale" in body) c.locale = parseLocale(body.locale);
|
|
2691
|
+
if ("preferences" in body) c.preferences = parsePreferences(c.preferences, body.preferences);
|
|
2676
2692
|
if ("fiscal" in body) {
|
|
2677
2693
|
if (body.fiscal === null) c.fiscal = null;
|
|
2678
2694
|
else {
|
|
@@ -2692,6 +2708,144 @@ function createMockStorefront(options = {}) {
|
|
|
2692
2708
|
}
|
|
2693
2709
|
return { body: publicCustomer(c), cache: "private" };
|
|
2694
2710
|
});
|
|
2711
|
+
const OPTIONAL_EVENTS = /* @__PURE__ */ new Set(["order_updates", "shipping", "restock"]);
|
|
2712
|
+
const MANDATORY_EVENTS = /* @__PURE__ */ new Set(["order_confirmation", "payment", "invoice", "security"]);
|
|
2713
|
+
const CHANNELS = /* @__PURE__ */ new Set(["email", "whatsapp"]);
|
|
2714
|
+
const REAUTH_WINDOW_MS = 5 * 6e4;
|
|
2715
|
+
const supportedLocales = () => [state.shop.locale, ...state.shop.locales].filter((l, i, a) => l && a.indexOf(l) === i);
|
|
2716
|
+
function parseLocale(raw) {
|
|
2717
|
+
if (raw === null || typeof raw === "string" && !raw.trim()) return null;
|
|
2718
|
+
if (typeof raw !== "string") throw new HttpError(400, "validation", "locale must be a string", "locale");
|
|
2719
|
+
const tag = raw.trim().replace("_", "-");
|
|
2720
|
+
const [lang = "", ...rest] = tag.split("-");
|
|
2721
|
+
const canonical = [lang.toLowerCase(), ...rest.map((p) => p.toUpperCase())].join("-");
|
|
2722
|
+
const supported = supportedLocales();
|
|
2723
|
+
const hit = supported.find((l) => l.toLowerCase() === canonical.toLowerCase()) ?? (rest.length === 0 ? supported.find((l) => l.split("-")[0].toLowerCase() === canonical) : void 0);
|
|
2724
|
+
if (!hit) throw new HttpError(400, "validation", "this store is not available in that language", "locale");
|
|
2725
|
+
return hit;
|
|
2726
|
+
}
|
|
2727
|
+
function parsePreferences(current, raw) {
|
|
2728
|
+
if (raw === null) return { notifications: {} };
|
|
2729
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "validation", "preferences must be an object", "preferences");
|
|
2730
|
+
const merged = structuredClone(current?.notifications ?? {});
|
|
2731
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2732
|
+
if (key !== "notifications") throw new HttpError(400, "validation", "unknown preference", `preferences.${key}`);
|
|
2733
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, "validation", "notifications must map each event to its channels", "preferences.notifications");
|
|
2734
|
+
for (const [event, channels] of Object.entries(value)) {
|
|
2735
|
+
const field = `preferences.notifications.${event}`;
|
|
2736
|
+
if (MANDATORY_EVENTS.has(event)) throw new HttpError(400, "validation", "this notification is required and cannot be turned off", field);
|
|
2737
|
+
if (!OPTIONAL_EVENTS.has(event)) throw new HttpError(400, "validation", "unknown notification", field);
|
|
2738
|
+
if (!channels || typeof channels !== "object" || Array.isArray(channels)) throw new HttpError(400, "validation", "each event maps its channels to true or false", field);
|
|
2739
|
+
for (const [channel, on] of Object.entries(channels)) {
|
|
2740
|
+
if (!CHANNELS.has(channel)) throw new HttpError(400, "validation", "unknown channel", `${field}.${channel}`);
|
|
2741
|
+
if (typeof on !== "boolean") throw new HttpError(400, "validation", "each channel must be true or false", `${field}.${channel}`);
|
|
2742
|
+
(merged[event] ??= {})[channel] = on;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
return { notifications: merged };
|
|
2747
|
+
}
|
|
2748
|
+
function sessionOf(headers) {
|
|
2749
|
+
const c = requireCustomer(headers);
|
|
2750
|
+
const token = headers.get("x-customer-token");
|
|
2751
|
+
if (!state.sessionInfo.has(token)) trackSession(token, "email");
|
|
2752
|
+
return { c, token, info: state.sessionInfo.get(token) };
|
|
2753
|
+
}
|
|
2754
|
+
function requireRecentReauth(info) {
|
|
2755
|
+
if (info.reauth_at === null || Date.now() - info.reauth_at > REAUTH_WINDOW_MS) {
|
|
2756
|
+
throw new HttpError(403, "reauth_required", "confirm your password or a code first (customer.reauth)");
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
route("GET", "/customer/sessions", "customer:read", ({ headers }) => {
|
|
2760
|
+
const { c, token } = sessionOf(headers);
|
|
2761
|
+
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 }) => ({
|
|
2762
|
+
id: info.id,
|
|
2763
|
+
current: t === token,
|
|
2764
|
+
device_label: "Mock browser \xB7 Test",
|
|
2765
|
+
login_method: info.login_method,
|
|
2766
|
+
last_used_at: info.last_used_at,
|
|
2767
|
+
created_at: info.created_at
|
|
2768
|
+
}));
|
|
2769
|
+
return { body: { nodes }, cache: "private" };
|
|
2770
|
+
});
|
|
2771
|
+
route("DELETE", "/customer/sessions/:id", "customer:write", ({ headers, params }) => {
|
|
2772
|
+
const { c, token } = sessionOf(headers);
|
|
2773
|
+
const mine = [...state.sessions.entries()].filter(([, id]) => id === c.id).map(([t]) => t);
|
|
2774
|
+
if (params[0] === "others") {
|
|
2775
|
+
for (const t of mine) if (t !== token) state.sessions.delete(t);
|
|
2776
|
+
return { status: 204 };
|
|
2777
|
+
}
|
|
2778
|
+
const entry = [...state.sessionInfo.entries()].find(([, i]) => i.id === params[0]);
|
|
2779
|
+
if (!entry || entry[1].customer_id !== c.id) throw new HttpError(404, "not_found", "Session not found", "id");
|
|
2780
|
+
state.sessions.delete(entry[0]);
|
|
2781
|
+
return { status: 204 };
|
|
2782
|
+
});
|
|
2783
|
+
route("POST", "/customer/reauth", "customer:write", ({ headers, body }) => {
|
|
2784
|
+
const { c, info } = sessionOf(headers);
|
|
2785
|
+
const hasPassword = typeof body.password === "string" && body.password !== "";
|
|
2786
|
+
const hasCode = typeof body.otp_code === "string" && body.otp_code !== "";
|
|
2787
|
+
if (hasPassword === hasCode) throw new HttpError(400, "validation", "send either password or otp_code", "password");
|
|
2788
|
+
if ((state.reauthFailures.get(c.id) ?? 0) >= 5) throw new HttpError(429, "rate_limited", "Too many attempts, try again later");
|
|
2789
|
+
const field = hasPassword ? "password" : "otp_code";
|
|
2790
|
+
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);
|
|
2791
|
+
if (!ok) {
|
|
2792
|
+
state.reauthFailures.set(c.id, (state.reauthFailures.get(c.id) ?? 0) + 1);
|
|
2793
|
+
throw new HttpError(400, "validation", "could not verify; check the credential and try again", field);
|
|
2794
|
+
}
|
|
2795
|
+
state.reauthFailures.delete(c.id);
|
|
2796
|
+
info.reauth_at = Date.now();
|
|
2797
|
+
return { body: { reauth_at: new Date(info.reauth_at).toISOString(), valid_for_seconds: REAUTH_WINDOW_MS / 1e3 }, cache: "private" };
|
|
2798
|
+
});
|
|
2799
|
+
route("POST", "/customer/data-export", "customer:write", ({ headers }) => {
|
|
2800
|
+
const { c, info } = sessionOf(headers);
|
|
2801
|
+
requireRecentReauth(info);
|
|
2802
|
+
if ((state.exportsServed.get(c.id) ?? 0) >= 3) throw new HttpError(429, "rate_limited", "Three exports a day; try again tomorrow");
|
|
2803
|
+
state.exportsServed.set(c.id, (state.exportsServed.get(c.id) ?? 0) + 1);
|
|
2804
|
+
const doc = {
|
|
2805
|
+
format: "storefront-customer-data/1",
|
|
2806
|
+
exported_at: now(),
|
|
2807
|
+
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 },
|
|
2808
|
+
preferences: c.preferences ?? { notifications: {} },
|
|
2809
|
+
addresses: state.addresses.get(c.id) ?? [],
|
|
2810
|
+
fiscal_profiles: state.fiscalProfiles.get(c.id) ?? [],
|
|
2811
|
+
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 }))
|
|
2812
|
+
};
|
|
2813
|
+
return {
|
|
2814
|
+
body: {
|
|
2815
|
+
filename: `account-data-${now().slice(0, 10)}.json`,
|
|
2816
|
+
content_type: "application/json",
|
|
2817
|
+
content_base64: btoa(unescape(encodeURIComponent(JSON.stringify(doc, null, 2))))
|
|
2818
|
+
},
|
|
2819
|
+
cache: "private"
|
|
2820
|
+
};
|
|
2821
|
+
});
|
|
2822
|
+
route("DELETE", "/customer", "customer:write", ({ headers, body }) => {
|
|
2823
|
+
const token = headers.get("x-customer-token");
|
|
2824
|
+
if (token && state.deletedTokens.has(token)) return { status: 204 };
|
|
2825
|
+
const { c, info } = sessionOf(headers);
|
|
2826
|
+
requireRecentReauth(info);
|
|
2827
|
+
if (typeof body.confirm !== "string" || !body.confirm.trim() || body.confirm.length > 100) {
|
|
2828
|
+
throw new HttpError(400, "validation", "confirm is required: the text the buyer typed", "confirm");
|
|
2829
|
+
}
|
|
2830
|
+
const closed = /* @__PURE__ */ new Set(["fulfilled", "delivered", "cancelled"]);
|
|
2831
|
+
if (state.orders.some((o) => o.customer_id === c.id && !closed.has(o.status))) {
|
|
2832
|
+
throw new HttpError(409, "conflict", "you have an order in progress; you can delete the account once it is delivered", "orders");
|
|
2833
|
+
}
|
|
2834
|
+
if (c.id === "cus_1" && state.pendingPayments.some((p) => p.status === "pending_payment")) {
|
|
2835
|
+
throw new HttpError(409, "conflict", "you have a payment waiting to be completed", "pending_payments");
|
|
2836
|
+
}
|
|
2837
|
+
for (const [t, id] of [...state.sessions.entries()]) {
|
|
2838
|
+
if (id === c.id) {
|
|
2839
|
+
state.sessions.delete(t);
|
|
2840
|
+
state.deletedTokens.add(t);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
if (c.email) state.passwords.delete(c.email);
|
|
2844
|
+
state.addresses.delete(c.id);
|
|
2845
|
+
state.fiscalProfiles.delete(c.id);
|
|
2846
|
+
state.customers = state.customers.filter((x) => x.id !== c.id);
|
|
2847
|
+
return { status: 204 };
|
|
2848
|
+
});
|
|
2695
2849
|
const orderView = ({ customer_id: _c, ...o }, detail) => {
|
|
2696
2850
|
const order = { ...o, invoice: orderInvoice(o) };
|
|
2697
2851
|
if (order.shipments) order.shipments = order.shipments.map((s) => isTestShipment(s) ? { ...s, tracking_number: null, tracking_url: null } : { ...s, events: sortTrackingEvents(s.events) });
|
|
@@ -2944,7 +3098,7 @@ function createMockStorefront(options = {}) {
|
|
|
2944
3098
|
return errorResponse(new HttpError(403, "insufficient_scope", `Token lacks scope ${r.scope}`, void 0, void 0, [r.scope]));
|
|
2945
3099
|
}
|
|
2946
3100
|
let body = {};
|
|
2947
|
-
if (method !== "GET"
|
|
3101
|
+
if (method !== "GET") {
|
|
2948
3102
|
const text = await request.text();
|
|
2949
3103
|
if (text) {
|
|
2950
3104
|
try {
|