@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/mock.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a2 as IdentityRulesV1, ba as RegionFiscalId, a0 as IdentityDocumentSpec, b8 as Region, br as Shop, aX as Product, x as Collection, by as Theme, aC as Page, al as Menu, b as Article, C as Cart, A as Address, ab as InvoiceRequest, z as Customer, X as FiscalProfile, as as Order, aF as PaymentAttemptResult, aU as PendingPayment, ad as InvoiceStage, B as CustomerCapabilities, b2 as PromotionAnnouncement, bq as ShippingRate, aM as PaymentMethod } from './types-C5nEQicj.cjs';
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 { a2 as IdentityRulesV1, ba as RegionFiscalId, a0 as IdentityDocumentSpec, b8 as Region, br as Shop, aX as Product, x as Collection, by as Theme, aC as Page, al as Menu, b as Article, C as Cart, A as Address, ab as InvoiceRequest, z as Customer, X as FiscalProfile, as as Order, aF as PaymentAttemptResult, aU as PendingPayment, ad as InvoiceStage, B as CustomerCapabilities, b2 as PromotionAnnouncement, bq as ShippingRate, aM as PaymentMethod } from './types-DSHzYSHU.js';
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 = (c) => {
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" && method !== "DELETE") {
2929
+ if (method !== "GET") {
2776
2930
  const text = await request.text();
2777
2931
  if (text) {
2778
2932
  try {