@asteby/storefront 0.8.0 → 0.10.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 { aB as Shop, ah as Product, w as Collection, aI as Theme, $ as Page, U as Menu, b as Article, C as Cart, A as Address, y as Customer, Z as Order, a2 as PaymentAttemptResult, ao as PromotionAnnouncement, aA as ShippingRate, a8 as PaymentMethod } from './types-CSMuPSQA.cjs';
1
+ import { aI as Shop, ao as Product, w as Collection, aP as Theme, a6 as Page, $ as Menu, b as Article, C as Cart, A as Address, y as Customer, a4 as Order, a9 as PaymentAttemptResult, z as CustomerCapabilities, av as PromotionAnnouncement, aH as ShippingRate, af as PaymentMethod } from './types-DdLzlxw4.cjs';
2
2
  import './geo.cjs';
3
3
 
4
4
  /**
@@ -31,6 +31,14 @@ type MockStorefrontOptions = {
31
31
  imageUrl?: MockImageUrl;
32
32
  /** OTP code accepted by `/customer/session` (default `000000`). */
33
33
  otpCode?: string;
34
+ /**
35
+ * What `GET /customer/capabilities` reports (default `{ password: true, email_verification: true, otp_channels: ['email'] }`).
36
+ * `password: false` makes register / login / password routes answer `403`; `email_verification: false` makes `register`
37
+ * create the account straight away (`201`).
38
+ */
39
+ customerCapabilities?: Partial<CustomerCapabilities>;
40
+ /** Failed password logins per email before `429 rate_limited` (default 5; a success or a reset clears the count). */
41
+ maxLoginAttempts?: number;
34
42
  /** Discount codes → percentage off the subtotal (default `{ BIENVENIDA10: 10 }`). */
35
43
  discountCodes?: Record<string, number>;
36
44
  /** Flat shipping rate (default 149). Free from `shop.policies.free_shipping_min`. */
@@ -96,6 +104,16 @@ type MockState = {
96
104
  customer_id: string | null;
97
105
  })[];
98
106
  otps: Map<string, string>;
107
+ /** Email → password (plain text: it is a mock). The seed customer starts without one. */
108
+ passwords: Map<string, string>;
109
+ /** Registrations waiting for their code, by email. */
110
+ pendingRegistrations: Map<string, {
111
+ password: string;
112
+ first_name: string | null;
113
+ last_name: string | null;
114
+ }>;
115
+ /** Consecutive failed password logins by email. */
116
+ loginFailures: Map<string, number>;
99
117
  sessions: Map<string, string>;
100
118
  idempotency: Map<string, {
101
119
  status: number;
package/dist/mock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { aB as Shop, ah as Product, w as Collection, aI as Theme, $ as Page, U as Menu, b as Article, C as Cart, A as Address, y as Customer, Z as Order, a2 as PaymentAttemptResult, ao as PromotionAnnouncement, aA as ShippingRate, a8 as PaymentMethod } from './types-iGGZ9EiC.js';
1
+ import { aI as Shop, ao as Product, w as Collection, aP as Theme, a6 as Page, $ as Menu, b as Article, C as Cart, A as Address, y as Customer, a4 as Order, a9 as PaymentAttemptResult, z as CustomerCapabilities, av as PromotionAnnouncement, aH as ShippingRate, af as PaymentMethod } from './types-COSwo4ND.js';
2
2
  import './geo.js';
3
3
 
4
4
  /**
@@ -31,6 +31,14 @@ type MockStorefrontOptions = {
31
31
  imageUrl?: MockImageUrl;
32
32
  /** OTP code accepted by `/customer/session` (default `000000`). */
33
33
  otpCode?: string;
34
+ /**
35
+ * What `GET /customer/capabilities` reports (default `{ password: true, email_verification: true, otp_channels: ['email'] }`).
36
+ * `password: false` makes register / login / password routes answer `403`; `email_verification: false` makes `register`
37
+ * create the account straight away (`201`).
38
+ */
39
+ customerCapabilities?: Partial<CustomerCapabilities>;
40
+ /** Failed password logins per email before `429 rate_limited` (default 5; a success or a reset clears the count). */
41
+ maxLoginAttempts?: number;
34
42
  /** Discount codes → percentage off the subtotal (default `{ BIENVENIDA10: 10 }`). */
35
43
  discountCodes?: Record<string, number>;
36
44
  /** Flat shipping rate (default 149). Free from `shop.policies.free_shipping_min`. */
@@ -96,6 +104,16 @@ type MockState = {
96
104
  customer_id: string | null;
97
105
  })[];
98
106
  otps: Map<string, string>;
107
+ /** Email → password (plain text: it is a mock). The seed customer starts without one. */
108
+ passwords: Map<string, string>;
109
+ /** Registrations waiting for their code, by email. */
110
+ pendingRegistrations: Map<string, {
111
+ password: string;
112
+ first_name: string | null;
113
+ last_name: string | null;
114
+ }>;
115
+ /** Consecutive failed password logins by email. */
116
+ loginFailures: Map<string, number>;
99
117
  sessions: Map<string, string>;
100
118
  idempotency: Map<string, {
101
119
  status: number;
package/dist/mock.js CHANGED
@@ -395,6 +395,15 @@ function createMockStorefront(options = {}) {
395
395
  const currency = options.currency ?? MOCK_CURRENCY;
396
396
  const imageUrl = options.imageUrl ?? MOCK_IMAGE_URL;
397
397
  const otpCode = options.otpCode ?? "000000";
398
+ const capabilities = {
399
+ password: true,
400
+ email_verification: true,
401
+ otp_channels: ["email"],
402
+ ...options.customerCapabilities
403
+ };
404
+ const maxLoginAttempts = options.maxLoginAttempts ?? 5;
405
+ const MIN_PASSWORD = 10;
406
+ const MAX_PASSWORD = 128;
398
407
  const discountCodes = Object.fromEntries(
399
408
  Object.entries(options.discountCodes ?? { BIENVENIDA10: 10 }).map(([k, v]) => [k.toUpperCase(), v])
400
409
  );
@@ -427,6 +436,9 @@ function createMockStorefront(options = {}) {
427
436
  }
428
437
  ],
429
438
  otps: /* @__PURE__ */ new Map(),
439
+ passwords: /* @__PURE__ */ new Map(),
440
+ pendingRegistrations: /* @__PURE__ */ new Map(),
441
+ loginFailures: /* @__PURE__ */ new Map(),
430
442
  sessions: /* @__PURE__ */ new Map(),
431
443
  idempotency: /* @__PURE__ */ new Map(),
432
444
  checkouts: /* @__PURE__ */ new Map(),
@@ -711,6 +723,39 @@ function createMockStorefront(options = {}) {
711
723
  if (email && !/^[^@\s]+@[^@\s]+$/.test(email)) throw new HttpError(400, "validation", "Invalid email", "email");
712
724
  return email ? `email:${email}` : `phone:${phone}`;
713
725
  }
726
+ function emailOf(body) {
727
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
728
+ if (!email || !/^[^@\s]+@[^@\s]+$/.test(email)) throw new HttpError(400, "validation", "Invalid email", "email");
729
+ return email;
730
+ }
731
+ function passwordOf(body, field = "password") {
732
+ const value = body[field];
733
+ if (typeof value !== "string" || value.length < MIN_PASSWORD || value.length > MAX_PASSWORD) {
734
+ throw new HttpError(400, "validation", `${field} must be ${MIN_PASSWORD}-${MAX_PASSWORD} characters`, field);
735
+ }
736
+ return value;
737
+ }
738
+ function requirePasswordEnabled() {
739
+ if (!capabilities.password) throw new HttpError(403, "forbidden", "Password accounts are disabled for this store");
740
+ }
741
+ function sessionFor(email, extra = {}) {
742
+ let customer = state.customers.find((c) => c.email === email);
743
+ if (!customer) {
744
+ customer = {
745
+ id: nextId("cus"),
746
+ email,
747
+ phone: null,
748
+ first_name: extra.first_name ?? null,
749
+ last_name: extra.last_name ?? null,
750
+ default_address_id: null,
751
+ created_at: now()
752
+ };
753
+ state.customers.push(customer);
754
+ }
755
+ const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
756
+ state.sessions.set(token, customer.id);
757
+ return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
758
+ }
714
759
  function validateAddress(body, partial = false) {
715
760
  const required = ["address1", "city", "zip", "country_code"];
716
761
  if (!partial) {
@@ -1126,6 +1171,74 @@ function createMockStorefront(options = {}) {
1126
1171
  cache: "private"
1127
1172
  };
1128
1173
  });
1174
+ route("GET", "/customer/capabilities", "customer:read", () => ({ body: capabilities, cache: "private" }));
1175
+ route("POST", "/customer/register", "customer:write", ({ body }) => {
1176
+ requirePasswordEnabled();
1177
+ const email = emailOf(body);
1178
+ const password = passwordOf(body);
1179
+ const text = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
1180
+ const first_name = text(body.first_name);
1181
+ const last_name = text(body.last_name);
1182
+ if (state.passwords.has(email)) throw new HttpError(409, "conflict", "An account with this email already exists", "email");
1183
+ if (capabilities.email_verification) {
1184
+ state.pendingRegistrations.set(email, { password, first_name, last_name });
1185
+ state.otps.set(`email:${email}`, otpCode);
1186
+ return { status: 202, body: { status: "verification_required" }, cache: "private" };
1187
+ }
1188
+ state.passwords.set(email, password);
1189
+ return { status: 201, body: sessionFor(email, { first_name, last_name }), cache: "private" };
1190
+ });
1191
+ route("POST", "/customer/register/verify", "customer:write", ({ body }) => {
1192
+ requirePasswordEnabled();
1193
+ const email = emailOf(body);
1194
+ const pending = state.pendingRegistrations.get(email);
1195
+ if (!pending || typeof body.code !== "string" || state.otps.get(`email:${email}`) !== body.code) {
1196
+ throw new HttpError(401, "unauthorized", "Invalid or expired code", "code");
1197
+ }
1198
+ state.pendingRegistrations.delete(email);
1199
+ state.otps.delete(`email:${email}`);
1200
+ state.passwords.set(email, pending.password);
1201
+ return { status: 201, body: sessionFor(email, pending), cache: "private" };
1202
+ });
1203
+ route("POST", "/customer/login", "customer:write", ({ body }) => {
1204
+ requirePasswordEnabled();
1205
+ const email = emailOf(body);
1206
+ if ((state.loginFailures.get(email) ?? 0) >= maxLoginAttempts) {
1207
+ throw new HttpError(429, "rate_limited", "Too many failed attempts, try again later");
1208
+ }
1209
+ const stored = state.passwords.get(email);
1210
+ if (stored === void 0 || typeof body.password !== "string" || body.password !== stored) {
1211
+ state.loginFailures.set(email, (state.loginFailures.get(email) ?? 0) + 1);
1212
+ throw new HttpError(401, "unauthorized", "Invalid email or password");
1213
+ }
1214
+ state.loginFailures.delete(email);
1215
+ return { status: 201, body: sessionFor(email), cache: "private" };
1216
+ });
1217
+ route("PUT", "/customer/password", "customer:write", ({ headers, body }) => {
1218
+ requirePasswordEnabled();
1219
+ const customer = requireCustomer(headers);
1220
+ const password = passwordOf(body);
1221
+ const email = customer.email;
1222
+ if (!email) throw new HttpError(422, "invalid_state", "The account has no email", "email");
1223
+ const current = state.passwords.get(email);
1224
+ if (current !== void 0 && body.current_password !== current) {
1225
+ throw new HttpError(401, "unauthorized", "current_password is missing or wrong", "current_password");
1226
+ }
1227
+ state.passwords.set(email, password);
1228
+ return { status: 204 };
1229
+ });
1230
+ route("POST", "/customer/password/reset", "customer:write", ({ body }) => {
1231
+ requirePasswordEnabled();
1232
+ const email = emailOf(body);
1233
+ const password = passwordOf(body);
1234
+ if (typeof body.code !== "string" || state.otps.get(`email:${email}`) !== body.code || !state.customers.some((c) => c.email === email)) {
1235
+ throw new HttpError(401, "unauthorized", "Invalid or expired code", "code");
1236
+ }
1237
+ state.otps.delete(`email:${email}`);
1238
+ state.passwords.set(email, password);
1239
+ state.loginFailures.delete(email);
1240
+ return { status: 204 };
1241
+ });
1129
1242
  route("GET", "/customer", "customer:read", ({ headers }) => ({ body: requireCustomer(headers), cache: "private" }));
1130
1243
  const stripOrder = ({ customer_id: _c, ...o }) => o;
1131
1244
  route("GET", "/customer/orders", "orders:read", ({ headers, search }) => {