@asteby/storefront 0.9.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.cjs CHANGED
@@ -438,6 +438,15 @@ function createMockStorefront(options = {}) {
438
438
  const currency = options.currency ?? MOCK_CURRENCY;
439
439
  const imageUrl = options.imageUrl ?? MOCK_IMAGE_URL;
440
440
  const otpCode = options.otpCode ?? "000000";
441
+ const capabilities = {
442
+ password: true,
443
+ email_verification: true,
444
+ otp_channels: ["email"],
445
+ ...options.customerCapabilities
446
+ };
447
+ const maxLoginAttempts = options.maxLoginAttempts ?? 5;
448
+ const MIN_PASSWORD = 10;
449
+ const MAX_PASSWORD = 128;
441
450
  const discountCodes = Object.fromEntries(
442
451
  Object.entries(options.discountCodes ?? { BIENVENIDA10: 10 }).map(([k, v]) => [k.toUpperCase(), v])
443
452
  );
@@ -470,6 +479,9 @@ function createMockStorefront(options = {}) {
470
479
  }
471
480
  ],
472
481
  otps: /* @__PURE__ */ new Map(),
482
+ passwords: /* @__PURE__ */ new Map(),
483
+ pendingRegistrations: /* @__PURE__ */ new Map(),
484
+ loginFailures: /* @__PURE__ */ new Map(),
473
485
  sessions: /* @__PURE__ */ new Map(),
474
486
  idempotency: /* @__PURE__ */ new Map(),
475
487
  checkouts: /* @__PURE__ */ new Map(),
@@ -754,6 +766,39 @@ function createMockStorefront(options = {}) {
754
766
  if (email && !/^[^@\s]+@[^@\s]+$/.test(email)) throw new HttpError(400, "validation", "Invalid email", "email");
755
767
  return email ? `email:${email}` : `phone:${phone}`;
756
768
  }
769
+ function emailOf(body) {
770
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
771
+ if (!email || !/^[^@\s]+@[^@\s]+$/.test(email)) throw new HttpError(400, "validation", "Invalid email", "email");
772
+ return email;
773
+ }
774
+ function passwordOf(body, field = "password") {
775
+ const value = body[field];
776
+ if (typeof value !== "string" || value.length < MIN_PASSWORD || value.length > MAX_PASSWORD) {
777
+ throw new HttpError(400, "validation", `${field} must be ${MIN_PASSWORD}-${MAX_PASSWORD} characters`, field);
778
+ }
779
+ return value;
780
+ }
781
+ function requirePasswordEnabled() {
782
+ if (!capabilities.password) throw new HttpError(403, "forbidden", "Password accounts are disabled for this store");
783
+ }
784
+ function sessionFor(email, extra = {}) {
785
+ let customer = state.customers.find((c) => c.email === email);
786
+ if (!customer) {
787
+ customer = {
788
+ id: nextId("cus"),
789
+ email,
790
+ phone: null,
791
+ first_name: extra.first_name ?? null,
792
+ last_name: extra.last_name ?? null,
793
+ default_address_id: null,
794
+ created_at: now()
795
+ };
796
+ state.customers.push(customer);
797
+ }
798
+ const token = `ct_${customer.id}_${Math.random().toString(36).slice(2)}`;
799
+ state.sessions.set(token, customer.id);
800
+ return { token, expires_at: new Date(Date.now() + 30 * 864e5).toISOString(), customer };
801
+ }
757
802
  function validateAddress(body, partial = false) {
758
803
  const required = ["address1", "city", "zip", "country_code"];
759
804
  if (!partial) {
@@ -1169,6 +1214,74 @@ function createMockStorefront(options = {}) {
1169
1214
  cache: "private"
1170
1215
  };
1171
1216
  });
1217
+ route("GET", "/customer/capabilities", "customer:read", () => ({ body: capabilities, cache: "private" }));
1218
+ route("POST", "/customer/register", "customer:write", ({ body }) => {
1219
+ requirePasswordEnabled();
1220
+ const email = emailOf(body);
1221
+ const password = passwordOf(body);
1222
+ const text = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
1223
+ const first_name = text(body.first_name);
1224
+ const last_name = text(body.last_name);
1225
+ if (state.passwords.has(email)) throw new HttpError(409, "conflict", "An account with this email already exists", "email");
1226
+ if (capabilities.email_verification) {
1227
+ state.pendingRegistrations.set(email, { password, first_name, last_name });
1228
+ state.otps.set(`email:${email}`, otpCode);
1229
+ return { status: 202, body: { status: "verification_required" }, cache: "private" };
1230
+ }
1231
+ state.passwords.set(email, password);
1232
+ return { status: 201, body: sessionFor(email, { first_name, last_name }), cache: "private" };
1233
+ });
1234
+ route("POST", "/customer/register/verify", "customer:write", ({ body }) => {
1235
+ requirePasswordEnabled();
1236
+ const email = emailOf(body);
1237
+ const pending = state.pendingRegistrations.get(email);
1238
+ if (!pending || typeof body.code !== "string" || state.otps.get(`email:${email}`) !== body.code) {
1239
+ throw new HttpError(401, "unauthorized", "Invalid or expired code", "code");
1240
+ }
1241
+ state.pendingRegistrations.delete(email);
1242
+ state.otps.delete(`email:${email}`);
1243
+ state.passwords.set(email, pending.password);
1244
+ return { status: 201, body: sessionFor(email, pending), cache: "private" };
1245
+ });
1246
+ route("POST", "/customer/login", "customer:write", ({ body }) => {
1247
+ requirePasswordEnabled();
1248
+ const email = emailOf(body);
1249
+ if ((state.loginFailures.get(email) ?? 0) >= maxLoginAttempts) {
1250
+ throw new HttpError(429, "rate_limited", "Too many failed attempts, try again later");
1251
+ }
1252
+ const stored = state.passwords.get(email);
1253
+ if (stored === void 0 || typeof body.password !== "string" || body.password !== stored) {
1254
+ state.loginFailures.set(email, (state.loginFailures.get(email) ?? 0) + 1);
1255
+ throw new HttpError(401, "unauthorized", "Invalid email or password");
1256
+ }
1257
+ state.loginFailures.delete(email);
1258
+ return { status: 201, body: sessionFor(email), cache: "private" };
1259
+ });
1260
+ route("PUT", "/customer/password", "customer:write", ({ headers, body }) => {
1261
+ requirePasswordEnabled();
1262
+ const customer = requireCustomer(headers);
1263
+ const password = passwordOf(body);
1264
+ const email = customer.email;
1265
+ if (!email) throw new HttpError(422, "invalid_state", "The account has no email", "email");
1266
+ const current = state.passwords.get(email);
1267
+ if (current !== void 0 && body.current_password !== current) {
1268
+ throw new HttpError(401, "unauthorized", "current_password is missing or wrong", "current_password");
1269
+ }
1270
+ state.passwords.set(email, password);
1271
+ return { status: 204 };
1272
+ });
1273
+ route("POST", "/customer/password/reset", "customer:write", ({ body }) => {
1274
+ requirePasswordEnabled();
1275
+ const email = emailOf(body);
1276
+ const password = passwordOf(body);
1277
+ if (typeof body.code !== "string" || state.otps.get(`email:${email}`) !== body.code || !state.customers.some((c) => c.email === email)) {
1278
+ throw new HttpError(401, "unauthorized", "Invalid or expired code", "code");
1279
+ }
1280
+ state.otps.delete(`email:${email}`);
1281
+ state.passwords.set(email, password);
1282
+ state.loginFailures.delete(email);
1283
+ return { status: 204 };
1284
+ });
1172
1285
  route("GET", "/customer", "customer:read", ({ headers }) => ({ body: requireCustomer(headers), cache: "private" }));
1173
1286
  const stripOrder = ({ customer_id: _c, ...o }) => o;
1174
1287
  route("GET", "/customer/orders", "orders:read", ({ headers, search }) => {