@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/README.md +33 -1
- package/dist/index.cjs +48 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -3
- package/dist/index.d.ts +49 -3
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +113 -0
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +19 -1
- package/dist/mock.d.ts +19 -1
- package/dist/mock.js +113 -0
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/{types-iGGZ9EiC.d.ts → types-COSwo4ND.d.ts} +41 -1
- package/dist/{types-CSMuPSQA.d.cts → types-DdLzlxw4.d.cts} +41 -1
- package/package.json +1 -1
package/dist/mock.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 }) => {
|