@asteby/storefront-react 0.11.1 → 0.13.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 +58 -0
- package/dist/account-server.cjs +152 -0
- package/dist/account-server.cjs.map +1 -0
- package/dist/account-server.d.cts +72 -0
- package/dist/account-server.d.ts +72 -0
- package/dist/account-server.js +147 -0
- package/dist/account-server.js.map +1 -0
- package/dist/account.cjs +432 -0
- package/dist/account.cjs.map +1 -0
- package/dist/account.css +40 -0
- package/dist/account.d.cts +66 -0
- package/dist/account.d.ts +66 -0
- package/dist/account.js +211 -0
- package/dist/account.js.map +1 -0
- package/dist/checkout.cjs +681 -51
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.css +230 -0
- package/dist/checkout.d.cts +228 -4
- package/dist/checkout.d.ts +228 -4
- package/dist/checkout.js +436 -45
- package/dist/checkout.js.map +1 -1
- package/dist/{chunk-ODIQR6TG.js → chunk-5WERSSU5.js} +13 -4
- package/dist/chunk-5WERSSU5.js.map +1 -0
- package/dist/chunk-DFGLCIEG.js +226 -0
- package/dist/chunk-DFGLCIEG.js.map +1 -0
- package/dist/fields-vnMUi2HB.d.cts +101 -0
- package/dist/fields-vnMUi2HB.d.ts +101 -0
- package/dist/payments.cjs +173 -39
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.css +6 -1
- package/dist/payments.d.cts +37 -1
- package/dist/payments.d.ts +37 -1
- package/dist/payments.js +164 -40
- package/dist/payments.js.map +1 -1
- package/dist/types-HcEUd9hW.d.cts +34 -0
- package/dist/types-HcEUd9hW.d.ts +34 -0
- package/package.json +23 -2
- package/dist/chunk-ODIQR6TG.js.map +0 -1
package/README.md
CHANGED
|
@@ -520,6 +520,10 @@ export const pay = createEmbeddedPayHandlers({
|
|
|
520
520
|
|
|
521
521
|
- Marcas (por omisión, `brandMarks`): la marca del proveedor en sus filas de cuenta y sin tarjeta, las de los primeros puntos de pago en la fila de efectivo y junto a cada punto (`payment_point` o `id`; `logo_url` del proveedor gana), y las redes que pase la tienda en la fila de tarjeta (`cardBrands={['visa', 'mastercard', 'amex']}`). `brandMarks={false}` deja las filas en texto.
|
|
522
522
|
- El total del botón siempre lleva la entrega: `syncDelivery` se llama una vez por entrega con cualquier fila (no solo con tarjeta); mientras se guarda, el botón no muestra un total viejo.
|
|
523
|
+
- El formulario de tarjeta se abre justo debajo de su fila (no al final de todas), con el correo del formulario de la página ya puesto (`payerEmailField`, por omisión `email`): el Brick no vuelve a pedirlo. En página oscura monta el tema base `dark` del proveedor (`readPaymentScheme`: token `--sf-pay-scheme` o `color-scheme`), refinado con los tokens `--sf-pay-*` (texto, fondos, contorno, radios, relleno).
|
|
524
|
+
- La elección se recuerda por carrito (`scope`) en `sessionStorage` (`rememberChoice`, por omisión sí): recargar, volver con «atrás» desde el proveedor o pasar por el paso de dirección no pide elegir de nuevo; si la fila ya no se publica, vuelve el valor por omisión del servidor. Solo se guarda el valor de la fila y el punto de efectivo.
|
|
525
|
+
- `cashPointsVisible={4}` deja a la vista los primeros puntos de efectivo y manda el resto a «Ver más puntos de pago» (se abre solo si el punto recordado está ahí).
|
|
526
|
+
- Botón: «Generar ficha de pago $X» con efectivo (`placeOrderCash`) y «Elige la entrega para continuar» mientras falta la entrega (`chooseDelivery`; al pulsarlo señala el campo que falta en vez de pagar).
|
|
523
527
|
|
|
524
528
|
## Entrega y marcas (`@asteby/storefront-react/checkout`)
|
|
525
529
|
|
|
@@ -536,8 +540,29 @@ import '@asteby/storefront-react/checkout.css' // opcional: disposición, insign
|
|
|
536
540
|
</fieldset>
|
|
537
541
|
```
|
|
538
542
|
|
|
543
|
+
Cotizaciones largas (`layout="recommended"`): con 10+ servicios, la lista plana obliga a comparar todo. Con `layout="recommended"` se ven hasta tres tarifas (`recommendedCount`) con su etiqueta: la más económica, la más rápida y la de mejor equilibrio precio-tiempo («Recomendado»); el resto queda en «Ver más opciones (N)» (`<details>`, funciona sin JavaScript) agrupado por paquetería, con la marca y el nombre UNA vez por grupo (cada radio conserva su nombre completo para lectores de pantalla). Los servicios repetidos (misma paquetería y servicio cotizados por dos proveedores) se quedan con el más barato. Si la tarifa elegida está dentro, la lista se abre sola (también en el HTML del servidor). La lógica pura está en `recommendRates()` / `dedupeRates()`. `error` (+ `onRetry`) muestra un aviso amable con «Intentar de nuevo» cuando no se pudo cotizar.
|
|
544
|
+
|
|
539
545
|
Tarifa vencida o cambiada: si guardar la entrega falla con `isQuoteChange(err)` (códigos `quote_expired`/`rate_expired`…, un 409, o el 422 genérico de servidores anteriores), la tienda recarga las tarifas conservando la dirección, vuelve a elegir el mismo servicio con `matchRate(anterior, nuevas)`, pasa `requoted` a `DeliveryOptions` y resuelve `syncDelivery` con `'requote'`: `PaymentMethodSection` muestra un aviso (no un error) y vuelve a intentar con la nueva tarifa.
|
|
540
546
|
|
|
547
|
+
### Campos, validación en línea y resumen del pedido
|
|
548
|
+
|
|
549
|
+
Piezas del formulario de pago iguales en todas las tiendas (también en `@asteby/storefront-react/checkout`):
|
|
550
|
+
|
|
551
|
+
- `ContactFields` y `AddressFields`: correo (`type="email"`, `inputMode="email"`), teléfono (`tel`), nombre (`given-name`) y apellidos (`family-name`); dirección en el orden en que se llena en México: código postal primero, estado, ciudad o municipio, colonia (`address-level3`, campo propio `neighborhood` de la API), calle y número, número interior (opcional) y referencias (opcional, `reference`). Sin país escrito en el código: el formato del código postal (`postalCode={{ pattern, maxLength, inputMode, title }}`), la lista de estados (`provinces`, de la configuración de la región) y la consulta por código postal (`lookupPostalCode`, una ruta propia sobre un catálogo) los pone la tienda. Con la consulta, estado y ciudad se llenan solos (nunca sobre lo que el comprador escribió) y las colonias salen como sugerencias (`<datalist>`). Sin ella, son campos de texto normales.
|
|
552
|
+
- `useFieldErrors(formRef)` + `CheckoutField` + `ErrorSummary`: validación en línea con las reglas del propio HTML (`required`, `type`, `pattern`…). Al salir de un campo con un valor inválido aparece el texto debajo del campo (`aria-invalid`, `aria-describedby`); se borra al corregirlo. Al enviar (o con `reportValidity()`, que usa `PaymentMethodSection`) los globos del navegador se reemplazan por los textos en línea y un resumen enfocable con enlaces a cada campo (WCAG 3.3.1). Los campos que rechace el servidor (`serverFields`) salen igual. Sin JavaScript, el navegador valida como siempre.
|
|
553
|
+
- `OrderSummary`: líneas y totales de `cart.cost` tal cual (subtotal, descuentos por código, envío «Al elegir la entrega» / «Actualizando…» / «Gratis», impuestos aparte o «Incluye $X de impuestos», total con `aria-live`). `OrderSummaryDisclosure` lo envuelve para teléfono: barra plegada arriba con el total («Ver resumen del pedido · $857.82»), `<details>` nativo, sin salto al hidratar. La tienda la muestra solo en pantallas angostas y el resumen fijo (`sticky`) en escritorio.
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
const v = useFieldErrors(formRef, { serverFields: actionData?.fields })
|
|
557
|
+
<ErrorSummary items={v.summary} focusKey={v.summaryKey} />
|
|
558
|
+
<ContactFields errors={v.errors} defaults={{ email: cart.buyer.email }} />
|
|
559
|
+
<AddressFields errors={v.errors} countryCode="MX" defaults={cart.shipping_address}
|
|
560
|
+
postalCode={{ pattern: '[0-9]{5}', maxLength: 5, inputMode: 'numeric', title: 'Escribe los 5 dígitos.' }} />
|
|
561
|
+
<OrderSummaryDisclosure total={cart.cost.total} className="lg:hidden">
|
|
562
|
+
<OrderSummary title={false} lines={cart.lines} cost={cart.cost} />
|
|
563
|
+
</OrderSummaryDisclosure>
|
|
564
|
+
```
|
|
565
|
+
|
|
541
566
|
Catálogo compartido de marcas (`brandKey`, `brandName`, `findBrand`, `<BrandMark brand="oxxo" />`): paqueterías (FedEx, DHL, Estafeta, Paquetexpress, UPS, Redpack, 99 Minutos, J&T…), redes de tarjeta (Visa, Mastercard, American Express, Carnet), billeteras (Mercado Pago, PayPal…) y puntos de efectivo y bancos de México (OXXO, SPEI/STP, 7-Eleven, Circle K, Soriana, Extra, BBVA…). La clave es el identificador que ya mandan los proveedores. Precedencia de lo que dibuja `<BrandMark>`: `logo_url` https del proveedor > logo real incluido > insignia tipográfica de respaldo (marcas sin logo disponible) > monograma neutro (marca desconocida). Los logos reales van incrustados como data URI (cero peticiones a terceros, mismo tamaño en servidor y navegador), sin recolorear, sobre una placa blanca con filo sutil para que se lean igual en tema claro y oscuro (`--sf-brand-mark-plate` y `--sf-brand-mark-ring` la ajustan). El elemento lleva `data-sf-brand-mark="<clave>"` y `data-sf-brand-source="provider|logo|badge"`; con `title` se anuncia con el nombre comercial, sin él es decorativo.
|
|
542
567
|
|
|
543
568
|
**Procedencia de los logos.** Son marcas registradas de sus titulares y se muestran sin alteración solo para identificar el servicio o método de pago que la tienda ofrece, sin sugerir patrocinio; cada tienda debe respetar las guías de marca de cada titular. Fuente, licencia, fecha y versión (color/monocroma) de cada uno, y las marcas que quedaron con insignia por no tener fuente con condiciones claras, están en `src/checkout/brands/LOGOS.md` (se regenera con `node scripts/build-logos.mjs`). Para usar un logo propio o licenciado: `<BrandMark brand="redpack" logoUrl="https://cdn.mitienda.mx/redpack.svg" title="Redpack" />`; en pagos, `PaymentPoint.logo_url`. Solo se acepta `https:`.
|
|
@@ -643,3 +668,36 @@ y define los tokens `--background --foreground --border --tint --primary --prima
|
|
|
643
668
|
| `ThemeToggle`, `THEME_INIT_SCRIPT` | Claro/oscuro con `html[data-theme]` y localStorage, sin parpadeo. |
|
|
644
669
|
| `InstallApp`, `useServiceWorker` | Instalación PWA (Android/Chrome y pista para iOS) y registro del service worker. |
|
|
645
670
|
| `.select`, `.check`, `.radio` | Controles de formulario con los tokens de la tienda. |
|
|
671
|
+
|
|
672
|
+
## Cuenta del comprador: `/account`
|
|
673
|
+
|
|
674
|
+
Pantalla de cuenta compartida (contraseña, crear cuenta, verificar código, entrar con código, olvidé mi contraseña). La tienda no escribe nada de esto: el loader pregunta qué admite OPS, la action la resuelve el SDK y el componente muestra solo lo que la tienda ofrece.
|
|
675
|
+
|
|
676
|
+
```tsx
|
|
677
|
+
// app/routes/account.login.tsx (React Router v7)
|
|
678
|
+
import { Form, useActionData, useLoaderData, useNavigation, useSearchParams } from 'react-router'
|
|
679
|
+
import { AccountAuth } from '@asteby/storefront-react/account'
|
|
680
|
+
import { createAuthAction, loadAuthCapabilities } from '@asteby/storefront-react/account/server'
|
|
681
|
+
import '@asteby/storefront-react/checkout.css' // campos
|
|
682
|
+
import '@asteby/storefront-react/account.css' // opcional
|
|
683
|
+
|
|
684
|
+
export const loader = () => loadAuthCapabilities(storefront) // caché de 60 s por cliente; nunca lanza
|
|
685
|
+
export const action = createAuthAction({
|
|
686
|
+
storefront, // cliente sk_
|
|
687
|
+
customerCookie, // createCookie('sf_customer', ...): serialize(token, { expires })
|
|
688
|
+
safeNext, // opcional; por omisión solo rutas del mismo sitio (safeNextPath)
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
export default function Login() {
|
|
692
|
+
const caps = useLoaderData<typeof loader>()
|
|
693
|
+
const data = useActionData<typeof action>()
|
|
694
|
+
const nav = useNavigation()
|
|
695
|
+
const [params] = useSearchParams()
|
|
696
|
+
return <AccountAuth capabilities={caps} data={data} next={params.get('next')} pending={nav.state !== 'idle'} form={Form} />
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
- `loadAuthCapabilities(sf, { ttlMs?, signal?, now? })` -> `CustomerCapabilities`. Si OPS no responde (servidor viejo, caída, token sin scope) devuelve `SAFE_AUTH_CAPABILITIES` (`{ password: true, email_verification: false, otp_channels: [] }`) y reintenta a los 5 s.
|
|
701
|
+
- `createAuthAction({ storefront, customerCookie, safeNext?, defaultNext?, clientIp?, capabilities? })` -> `({ request }) => Promise<Response | AuthActionData>`. Un solo `POST` con campo `intent`: `login`, `register`, `verify`, `otp-request`, `otp-login`, `forgot`, `reset`. Éxito: `303` a `next` con `Set-Cookie` de la sesión. Si no, `AuthActionData` (`{ ok, step?, email?, error?: { code, field? } }`, sin textos ni contraseñas). `forgot` siempre avanza a `reset` (no revela qué correos tienen cuenta).
|
|
702
|
+
- `<AccountAuth>`: `classNames` por pieza, atributos `data-sf-account-*`, `locale` (`es` por omisión, `en`), `messages` para cambiar cualquier texto, `form` para el `Form` del router. Con el mock (`createMockStorefront`) funciona entera: código `000000`.
|
|
703
|
+
- Sin JavaScript funciona el inicio de sesión con contraseña y los pasos que devuelve la action; el cambio entre pantallas (enlaces) necesita JavaScript.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var storefront = require('@asteby/storefront');
|
|
4
|
+
|
|
5
|
+
// src/account/server.ts
|
|
6
|
+
var SAFE_AUTH_CAPABILITIES = Object.freeze({
|
|
7
|
+
password: true,
|
|
8
|
+
email_verification: false,
|
|
9
|
+
otp_channels: Object.freeze([])
|
|
10
|
+
});
|
|
11
|
+
var FAILURE_TTL_MS = 5e3;
|
|
12
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
async function loadAuthCapabilities(sf, options = {}) {
|
|
14
|
+
const now = options.now ?? Date.now;
|
|
15
|
+
const ttl = options.ttlMs ?? 6e4;
|
|
16
|
+
const hit = cache.get(sf);
|
|
17
|
+
if (hit && now() - hit.at < (hit.ok ? ttl : FAILURE_TTL_MS)) return hit.value;
|
|
18
|
+
try {
|
|
19
|
+
const got = await sf.customer.capabilities(options.signal ? { signal: options.signal } : void 0);
|
|
20
|
+
const value = {
|
|
21
|
+
password: got?.password !== false,
|
|
22
|
+
email_verification: got?.email_verification === true,
|
|
23
|
+
otp_channels: Array.isArray(got?.otp_channels) ? got.otp_channels : []
|
|
24
|
+
};
|
|
25
|
+
cache.set(sf, { at: now(), ok: true, value });
|
|
26
|
+
return value;
|
|
27
|
+
} catch {
|
|
28
|
+
cache.set(sf, { at: now(), ok: false, value: SAFE_AUTH_CAPABILITIES });
|
|
29
|
+
return SAFE_AUTH_CAPABILITIES;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function safeNextPath(raw, fallback = "/account") {
|
|
33
|
+
if (typeof raw !== "string") return fallback;
|
|
34
|
+
const v = raw.trim();
|
|
35
|
+
if (!v.startsWith("/") || v.startsWith("//") || v.startsWith("/\\") || /[\u0000-\u001f\\]/.test(v)) return fallback;
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
var fail = (code, extra = {}, field) => ({
|
|
39
|
+
ok: false,
|
|
40
|
+
...extra,
|
|
41
|
+
error: { code, ...field ? { field } : {} }
|
|
42
|
+
});
|
|
43
|
+
function toFailure(err, ctx, extra) {
|
|
44
|
+
if (!storefront.isStorefrontError(err)) return fail("unavailable", extra);
|
|
45
|
+
switch (err.status) {
|
|
46
|
+
case 401:
|
|
47
|
+
return fail(ctx === "credentials" ? "invalid_credentials" : ctx === "code" ? "invalid_code" : "unavailable", extra, ctx === "code" ? "code" : void 0);
|
|
48
|
+
case 409:
|
|
49
|
+
return fail("email_taken", extra, "email");
|
|
50
|
+
case 429:
|
|
51
|
+
return fail("rate_limited", extra);
|
|
52
|
+
case 400:
|
|
53
|
+
case 422:
|
|
54
|
+
return fail("validation", extra, err.field);
|
|
55
|
+
case 403:
|
|
56
|
+
case 404:
|
|
57
|
+
return fail("unsupported", extra);
|
|
58
|
+
default:
|
|
59
|
+
return fail("unavailable", extra);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createAuthAction(options) {
|
|
63
|
+
const { storefront: sf, customerCookie } = options;
|
|
64
|
+
const safeNext = options.safeNext ?? ((raw) => safeNextPath(raw, options.defaultNext ?? "/account"));
|
|
65
|
+
const loadCaps = options.capabilities ?? ((client) => loadAuthCapabilities(client));
|
|
66
|
+
async function signedIn(session, next) {
|
|
67
|
+
const expires = new Date(session.expires_at);
|
|
68
|
+
const cookie = await customerCookie.serialize(session.token, Number.isNaN(expires.getTime()) ? void 0 : { expires });
|
|
69
|
+
return new Response(null, { status: 303, headers: { Location: next, "Set-Cookie": cookie } });
|
|
70
|
+
}
|
|
71
|
+
return async function authAction({ request }) {
|
|
72
|
+
const form = await request.formData().catch(() => null);
|
|
73
|
+
if (!form) return fail("validation");
|
|
74
|
+
const text = (name) => {
|
|
75
|
+
const v = form.get(name);
|
|
76
|
+
return typeof v === "string" ? v : "";
|
|
77
|
+
};
|
|
78
|
+
const intent = text("intent");
|
|
79
|
+
const email = text("email").trim();
|
|
80
|
+
const password = text("password");
|
|
81
|
+
const code = text("code").replace(/\s+/g, "");
|
|
82
|
+
const next = safeNext(text("next") || null);
|
|
83
|
+
const ip = options.clientIp?.(request);
|
|
84
|
+
const w = ip ? { clientIp: ip } : void 0;
|
|
85
|
+
const carry = { email };
|
|
86
|
+
if (!email) return fail("validation", carry, "email");
|
|
87
|
+
const caps = await loadCaps(sf);
|
|
88
|
+
const passwordOn = caps.password;
|
|
89
|
+
const otpOn = caps.otp_channels.includes("email");
|
|
90
|
+
try {
|
|
91
|
+
switch (intent) {
|
|
92
|
+
case "login": {
|
|
93
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
94
|
+
if (!password) return fail("validation", carry, "password");
|
|
95
|
+
return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
|
|
96
|
+
}
|
|
97
|
+
case "register": {
|
|
98
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
99
|
+
if (!password) return fail("validation", carry, "password");
|
|
100
|
+
const res = await sf.customer.register(
|
|
101
|
+
{ email, password, ...text("first_name").trim() ? { first_name: text("first_name").trim() } : {}, ...text("last_name").trim() ? { last_name: text("last_name").trim() } : {} },
|
|
102
|
+
w
|
|
103
|
+
);
|
|
104
|
+
return res.status === "created" ? await signedIn(res.session, next) : { ok: true, step: "verify", email };
|
|
105
|
+
}
|
|
106
|
+
case "verify": {
|
|
107
|
+
if (!code) return fail("validation", carry, "code");
|
|
108
|
+
return await signedIn(await sf.customer.verifyRegistration({ email, code }, w), next);
|
|
109
|
+
}
|
|
110
|
+
case "otp-request": {
|
|
111
|
+
if (!otpOn) return fail("unsupported", carry);
|
|
112
|
+
await sf.customer.requestOtp({ email }, w);
|
|
113
|
+
return { ok: true, step: "otp", email };
|
|
114
|
+
}
|
|
115
|
+
case "otp-login": {
|
|
116
|
+
if (!otpOn) return fail("unsupported", carry);
|
|
117
|
+
if (!code) return fail("validation", carry, "code");
|
|
118
|
+
return await signedIn(await sf.customer.createSession({ email, code }, w), next);
|
|
119
|
+
}
|
|
120
|
+
case "forgot": {
|
|
121
|
+
if (!passwordOn || !otpOn) return fail("unsupported", carry);
|
|
122
|
+
try {
|
|
123
|
+
await sf.customer.requestOtp({ email }, w);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (storefront.isStorefrontError(err) && (err.status === 429 || err.status === 400)) throw err;
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, step: "reset", email };
|
|
128
|
+
}
|
|
129
|
+
case "reset": {
|
|
130
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
131
|
+
if (!code) return fail("validation", carry, "code");
|
|
132
|
+
if (!password) return fail("validation", carry, "password");
|
|
133
|
+
await sf.customer.resetPassword({ email, code, password }, w);
|
|
134
|
+
return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return fail("validation", carry);
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const step = intent === "verify" ? "verify" : intent === "otp-login" ? "otp" : intent === "reset" || intent === "forgot" ? "reset" : void 0;
|
|
141
|
+
const ctx = intent === "login" ? "credentials" : step ? "code" : "other";
|
|
142
|
+
return toFailure(err, ctx, step && intent !== "forgot" ? { ...carry, step } : carry);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
exports.SAFE_AUTH_CAPABILITIES = SAFE_AUTH_CAPABILITIES;
|
|
148
|
+
exports.createAuthAction = createAuthAction;
|
|
149
|
+
exports.loadAuthCapabilities = loadAuthCapabilities;
|
|
150
|
+
exports.safeNextPath = safeNextPath;
|
|
151
|
+
//# sourceMappingURL=account-server.cjs.map
|
|
152
|
+
//# sourceMappingURL=account-server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/account/server.ts"],"names":["isStorefrontError"],"mappings":"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","file":"account-server.cjs","sourcesContent":["/**\n * Server side of the account screen (`@asteby/storefront-react/account/server`): `loadAuthCapabilities` for the loader\n * and `createAuthAction` for the action. No React, no router: Web `Request`/`Response` only, so any framework mounts it.\n *\n * The browser never talks to OPS. The action signs the buyer in with the store's `sk_` client and keeps the session\n * token in the store's own cookie, which is passed in (`customerCookie`), so name, flags and signing stay the store's.\n */\nimport { isStorefrontError, type CustomerCapabilities, type CustomerSession, type StorefrontClient } from '@asteby/storefront'\nimport type { AuthActionData, AuthErrorCode, AuthIntent } from './types.js'\n\nexport type { AuthActionData, AuthErrorCode, AuthIntent, AuthStep } from './types.js'\n\n/** What is assumed when the store cannot be asked: password sign-in works, nothing else is offered. */\nexport const SAFE_AUTH_CAPABILITIES: Readonly<CustomerCapabilities> = Object.freeze({\n password: true,\n email_verification: false,\n otp_channels: Object.freeze([]) as unknown as CustomerCapabilities['otp_channels'],\n})\n\nconst FAILURE_TTL_MS = 5_000\nconst cache = new WeakMap<object, { at: number; ok: boolean; value: CustomerCapabilities }>()\n\nexport type LoadAuthCapabilitiesOptions = {\n /** How long a good answer is reused per client (default 60 s). `0` disables the cache. */\n ttlMs?: number\n signal?: AbortSignal\n /** Clock, for tests. */\n now?: () => number\n}\n\n/**\n * Asks the store which ways of signing in it offers, with a short cache per client. It NEVER throws: when OPS does not\n * answer (older server, outage, token without scope) it returns `SAFE_AUTH_CAPABILITIES` and retries after a few\n * seconds, so the account screen always renders. Use it as the loader: `loader = () => loadAuthCapabilities(storefront)`.\n */\nexport async function loadAuthCapabilities(sf: StorefrontClient, options: LoadAuthCapabilitiesOptions = {}): Promise<CustomerCapabilities> {\n const now = options.now ?? Date.now\n const ttl = options.ttlMs ?? 60_000\n const hit = cache.get(sf)\n if (hit && now() - hit.at < (hit.ok ? ttl : FAILURE_TTL_MS)) return hit.value\n try {\n const got = await sf.customer.capabilities(options.signal ? { signal: options.signal } : undefined)\n const value: CustomerCapabilities = {\n password: got?.password !== false,\n email_verification: got?.email_verification === true,\n otp_channels: Array.isArray(got?.otp_channels) ? got.otp_channels : [],\n }\n cache.set(sf, { at: now(), ok: true, value })\n return value\n } catch {\n cache.set(sf, { at: now(), ok: false, value: SAFE_AUTH_CAPABILITIES })\n return SAFE_AUTH_CAPABILITIES\n }\n}\n\n/** The part of a cookie the action needs. React Router's `createCookie(...)` and Remix's fit as they are. */\nexport type AuthCookie = {\n serialize(value: string, options?: { expires?: Date; maxAge?: number }): string | Promise<string>\n}\n\n/** Allows only a path inside the site (`/account`, `/orders?x=1`); anything else is `fallback`. Blocks `//host` and `/\\host`. */\nexport function safeNextPath(raw: string | null | undefined, fallback = '/account'): string {\n if (typeof raw !== 'string') return fallback\n const v = raw.trim()\n if (!v.startsWith('/') || v.startsWith('//') || v.startsWith('/\\\\') || /[\\u0000-\\u001f\\\\]/.test(v)) return fallback\n return v\n}\n\nexport type CreateAuthActionOptions = {\n storefront: StorefrontClient\n /** The store's cookie for the buyer session token (`createCookie('sf_customer', ...)`). */\n customerCookie: AuthCookie\n /** Validates the `next` field before redirecting (default `safeNextPath`). Return a same-site path. */\n safeNext?: (raw: string | null) => string\n /** Where to go after signing in when there is no `next` (default `/account`). Only used by the default `safeNext`. */\n defaultNext?: string\n /** Shopper IP for the per-shopper rate limit (see `clientIp` of the client). */\n clientIp?: (request: Request) => string | null | undefined\n /** Capabilities source, for tests or a shared cache (default `loadAuthCapabilities`). */\n capabilities?: (sf: StorefrontClient) => Promise<CustomerCapabilities>\n}\n\nexport type AuthActionArgs = { request: Request }\n\nconst fail = (code: AuthErrorCode, extra: Partial<AuthActionData> = {}, field?: string): AuthActionData => ({\n ok: false,\n ...extra,\n error: { code, ...(field ? { field } : {}) },\n})\n\n/** Maps what the SDK threw to the error the form shows. `ctx` tells what a 401 means on this step. */\nfunction toFailure(err: unknown, ctx: 'credentials' | 'code' | 'other', extra: Partial<AuthActionData>): AuthActionData {\n if (!isStorefrontError(err)) return fail('unavailable', extra)\n switch (err.status) {\n case 401:\n return fail(ctx === 'credentials' ? 'invalid_credentials' : ctx === 'code' ? 'invalid_code' : 'unavailable', extra, ctx === 'code' ? 'code' : undefined)\n case 409:\n return fail('email_taken', extra, 'email')\n case 429:\n return fail('rate_limited', extra)\n case 400:\n case 422:\n return fail('validation', extra, err.field)\n case 403:\n case 404:\n return fail('unsupported', extra)\n default:\n return fail('unavailable', extra)\n }\n}\n\n/**\n * The account action: one `POST` form with an `intent` field.\n *\n * | intent | fields | result |\n * | ------------- | ---------------------------------------- | -------------------------------------------------------- |\n * | `login` | email, password | signs in -> redirect |\n * | `register` | email, password, first_name?, last_name? | `step: 'verify'` or, without verification, redirect |\n * | `verify` | email, code | account ready -> redirect |\n * | `otp-request` | email | `step: 'otp'` (code sent) |\n * | `otp-login` | email, code | signs in -> redirect |\n * | `forgot` | email | `step: 'reset'` (always, so it never says who has an account) |\n * | `reset` | email, code, password | new password and signed in -> redirect |\n *\n * Success is a `303` to `next` (validated by `safeNext`) with `Set-Cookie` holding the session token; anything else is\n * an `AuthActionData` to render with `<AccountAuth data={...}>`. Passwords and codes are never echoed back.\n */\nexport function createAuthAction(options: CreateAuthActionOptions) {\n const { storefront: sf, customerCookie } = options\n const safeNext = options.safeNext ?? ((raw: string | null) => safeNextPath(raw, options.defaultNext ?? '/account'))\n const loadCaps = options.capabilities ?? ((client: StorefrontClient) => loadAuthCapabilities(client))\n\n async function signedIn(session: CustomerSession, next: string): Promise<Response> {\n const expires = new Date(session.expires_at)\n const cookie = await customerCookie.serialize(session.token, Number.isNaN(expires.getTime()) ? undefined : { expires })\n return new Response(null, { status: 303, headers: { Location: next, 'Set-Cookie': cookie } })\n }\n\n return async function authAction({ request }: AuthActionArgs): Promise<Response | AuthActionData> {\n const form = await request.formData().catch(() => null)\n if (!form) return fail('validation')\n const text = (name: string) => {\n const v = form.get(name)\n return typeof v === 'string' ? v : ''\n }\n const intent = text('intent') as AuthIntent\n const email = text('email').trim()\n const password = text('password')\n const code = text('code').replace(/\\s+/g, '')\n const next = safeNext(text('next') || null)\n const ip = options.clientIp?.(request)\n const w = ip ? { clientIp: ip } : undefined\n const carry: Partial<AuthActionData> = { email }\n if (!email) return fail('validation', carry, 'email')\n\n const caps = await loadCaps(sf)\n const passwordOn = caps.password\n const otpOn = caps.otp_channels.includes('email')\n\n try {\n switch (intent) {\n case 'login': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!password) return fail('validation', carry, 'password')\n return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next)\n }\n case 'register': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!password) return fail('validation', carry, 'password')\n const res = await sf.customer.register(\n { email, password, ...(text('first_name').trim() ? { first_name: text('first_name').trim() } : {}), ...(text('last_name').trim() ? { last_name: text('last_name').trim() } : {}) },\n w,\n )\n return res.status === 'created' ? await signedIn(res.session, next) : { ok: true, step: 'verify', email }\n }\n case 'verify': {\n if (!code) return fail('validation', carry, 'code')\n return await signedIn(await sf.customer.verifyRegistration({ email, code }, w), next)\n }\n case 'otp-request': {\n if (!otpOn) return fail('unsupported', carry)\n await sf.customer.requestOtp({ email }, w)\n return { ok: true, step: 'otp', email }\n }\n case 'otp-login': {\n if (!otpOn) return fail('unsupported', carry)\n if (!code) return fail('validation', carry, 'code')\n return await signedIn(await sf.customer.createSession({ email, code }, w), next)\n }\n case 'forgot': {\n if (!passwordOn || !otpOn) return fail('unsupported', carry)\n try {\n await sf.customer.requestOtp({ email }, w)\n } catch (err) {\n // Only throttling and a bad email are told; anything else looks the same as success (no account enumeration).\n if (isStorefrontError(err) && (err.status === 429 || err.status === 400)) throw err\n }\n return { ok: true, step: 'reset', email }\n }\n case 'reset': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!code) return fail('validation', carry, 'code')\n if (!password) return fail('validation', carry, 'password')\n await sf.customer.resetPassword({ email, code, password }, w)\n return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next)\n }\n default:\n return fail('validation', carry)\n }\n } catch (err) {\n const step = intent === 'verify' ? 'verify' : intent === 'otp-login' ? 'otp' : intent === 'reset' || intent === 'forgot' ? 'reset' : undefined\n const ctx = intent === 'login' ? 'credentials' : step ? 'code' : 'other'\n return toFailure(err, ctx, step && intent !== 'forgot' ? { ...carry, step } : carry)\n }\n }\n}\n\nexport type AuthAction = ReturnType<typeof createAuthAction>\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { StorefrontClient, CustomerCapabilities } from '@asteby/storefront';
|
|
2
|
+
import { A as AuthActionData } from './types-HcEUd9hW.cjs';
|
|
3
|
+
export { a as AuthErrorCode, b as AuthIntent, c as AuthStep } from './types-HcEUd9hW.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server side of the account screen (`@asteby/storefront-react/account/server`): `loadAuthCapabilities` for the loader
|
|
7
|
+
* and `createAuthAction` for the action. No React, no router: Web `Request`/`Response` only, so any framework mounts it.
|
|
8
|
+
*
|
|
9
|
+
* The browser never talks to OPS. The action signs the buyer in with the store's `sk_` client and keeps the session
|
|
10
|
+
* token in the store's own cookie, which is passed in (`customerCookie`), so name, flags and signing stay the store's.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What is assumed when the store cannot be asked: password sign-in works, nothing else is offered. */
|
|
14
|
+
declare const SAFE_AUTH_CAPABILITIES: Readonly<CustomerCapabilities>;
|
|
15
|
+
type LoadAuthCapabilitiesOptions = {
|
|
16
|
+
/** How long a good answer is reused per client (default 60 s). `0` disables the cache. */
|
|
17
|
+
ttlMs?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Clock, for tests. */
|
|
20
|
+
now?: () => number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Asks the store which ways of signing in it offers, with a short cache per client. It NEVER throws: when OPS does not
|
|
24
|
+
* answer (older server, outage, token without scope) it returns `SAFE_AUTH_CAPABILITIES` and retries after a few
|
|
25
|
+
* seconds, so the account screen always renders. Use it as the loader: `loader = () => loadAuthCapabilities(storefront)`.
|
|
26
|
+
*/
|
|
27
|
+
declare function loadAuthCapabilities(sf: StorefrontClient, options?: LoadAuthCapabilitiesOptions): Promise<CustomerCapabilities>;
|
|
28
|
+
/** The part of a cookie the action needs. React Router's `createCookie(...)` and Remix's fit as they are. */
|
|
29
|
+
type AuthCookie = {
|
|
30
|
+
serialize(value: string, options?: {
|
|
31
|
+
expires?: Date;
|
|
32
|
+
maxAge?: number;
|
|
33
|
+
}): string | Promise<string>;
|
|
34
|
+
};
|
|
35
|
+
/** Allows only a path inside the site (`/account`, `/orders?x=1`); anything else is `fallback`. Blocks `//host` and `/\host`. */
|
|
36
|
+
declare function safeNextPath(raw: string | null | undefined, fallback?: string): string;
|
|
37
|
+
type CreateAuthActionOptions = {
|
|
38
|
+
storefront: StorefrontClient;
|
|
39
|
+
/** The store's cookie for the buyer session token (`createCookie('sf_customer', ...)`). */
|
|
40
|
+
customerCookie: AuthCookie;
|
|
41
|
+
/** Validates the `next` field before redirecting (default `safeNextPath`). Return a same-site path. */
|
|
42
|
+
safeNext?: (raw: string | null) => string;
|
|
43
|
+
/** Where to go after signing in when there is no `next` (default `/account`). Only used by the default `safeNext`. */
|
|
44
|
+
defaultNext?: string;
|
|
45
|
+
/** Shopper IP for the per-shopper rate limit (see `clientIp` of the client). */
|
|
46
|
+
clientIp?: (request: Request) => string | null | undefined;
|
|
47
|
+
/** Capabilities source, for tests or a shared cache (default `loadAuthCapabilities`). */
|
|
48
|
+
capabilities?: (sf: StorefrontClient) => Promise<CustomerCapabilities>;
|
|
49
|
+
};
|
|
50
|
+
type AuthActionArgs = {
|
|
51
|
+
request: Request;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The account action: one `POST` form with an `intent` field.
|
|
55
|
+
*
|
|
56
|
+
* | intent | fields | result |
|
|
57
|
+
* | ------------- | ---------------------------------------- | -------------------------------------------------------- |
|
|
58
|
+
* | `login` | email, password | signs in -> redirect |
|
|
59
|
+
* | `register` | email, password, first_name?, last_name? | `step: 'verify'` or, without verification, redirect |
|
|
60
|
+
* | `verify` | email, code | account ready -> redirect |
|
|
61
|
+
* | `otp-request` | email | `step: 'otp'` (code sent) |
|
|
62
|
+
* | `otp-login` | email, code | signs in -> redirect |
|
|
63
|
+
* | `forgot` | email | `step: 'reset'` (always, so it never says who has an account) |
|
|
64
|
+
* | `reset` | email, code, password | new password and signed in -> redirect |
|
|
65
|
+
*
|
|
66
|
+
* Success is a `303` to `next` (validated by `safeNext`) with `Set-Cookie` holding the session token; anything else is
|
|
67
|
+
* an `AuthActionData` to render with `<AccountAuth data={...}>`. Passwords and codes are never echoed back.
|
|
68
|
+
*/
|
|
69
|
+
declare function createAuthAction(options: CreateAuthActionOptions): ({ request }: AuthActionArgs) => Promise<Response | AuthActionData>;
|
|
70
|
+
type AuthAction = ReturnType<typeof createAuthAction>;
|
|
71
|
+
|
|
72
|
+
export { type AuthAction, type AuthActionArgs, AuthActionData, type AuthCookie, type CreateAuthActionOptions, type LoadAuthCapabilitiesOptions, SAFE_AUTH_CAPABILITIES, createAuthAction, loadAuthCapabilities, safeNextPath };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { StorefrontClient, CustomerCapabilities } from '@asteby/storefront';
|
|
2
|
+
import { A as AuthActionData } from './types-HcEUd9hW.js';
|
|
3
|
+
export { a as AuthErrorCode, b as AuthIntent, c as AuthStep } from './types-HcEUd9hW.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server side of the account screen (`@asteby/storefront-react/account/server`): `loadAuthCapabilities` for the loader
|
|
7
|
+
* and `createAuthAction` for the action. No React, no router: Web `Request`/`Response` only, so any framework mounts it.
|
|
8
|
+
*
|
|
9
|
+
* The browser never talks to OPS. The action signs the buyer in with the store's `sk_` client and keeps the session
|
|
10
|
+
* token in the store's own cookie, which is passed in (`customerCookie`), so name, flags and signing stay the store's.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What is assumed when the store cannot be asked: password sign-in works, nothing else is offered. */
|
|
14
|
+
declare const SAFE_AUTH_CAPABILITIES: Readonly<CustomerCapabilities>;
|
|
15
|
+
type LoadAuthCapabilitiesOptions = {
|
|
16
|
+
/** How long a good answer is reused per client (default 60 s). `0` disables the cache. */
|
|
17
|
+
ttlMs?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Clock, for tests. */
|
|
20
|
+
now?: () => number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Asks the store which ways of signing in it offers, with a short cache per client. It NEVER throws: when OPS does not
|
|
24
|
+
* answer (older server, outage, token without scope) it returns `SAFE_AUTH_CAPABILITIES` and retries after a few
|
|
25
|
+
* seconds, so the account screen always renders. Use it as the loader: `loader = () => loadAuthCapabilities(storefront)`.
|
|
26
|
+
*/
|
|
27
|
+
declare function loadAuthCapabilities(sf: StorefrontClient, options?: LoadAuthCapabilitiesOptions): Promise<CustomerCapabilities>;
|
|
28
|
+
/** The part of a cookie the action needs. React Router's `createCookie(...)` and Remix's fit as they are. */
|
|
29
|
+
type AuthCookie = {
|
|
30
|
+
serialize(value: string, options?: {
|
|
31
|
+
expires?: Date;
|
|
32
|
+
maxAge?: number;
|
|
33
|
+
}): string | Promise<string>;
|
|
34
|
+
};
|
|
35
|
+
/** Allows only a path inside the site (`/account`, `/orders?x=1`); anything else is `fallback`. Blocks `//host` and `/\host`. */
|
|
36
|
+
declare function safeNextPath(raw: string | null | undefined, fallback?: string): string;
|
|
37
|
+
type CreateAuthActionOptions = {
|
|
38
|
+
storefront: StorefrontClient;
|
|
39
|
+
/** The store's cookie for the buyer session token (`createCookie('sf_customer', ...)`). */
|
|
40
|
+
customerCookie: AuthCookie;
|
|
41
|
+
/** Validates the `next` field before redirecting (default `safeNextPath`). Return a same-site path. */
|
|
42
|
+
safeNext?: (raw: string | null) => string;
|
|
43
|
+
/** Where to go after signing in when there is no `next` (default `/account`). Only used by the default `safeNext`. */
|
|
44
|
+
defaultNext?: string;
|
|
45
|
+
/** Shopper IP for the per-shopper rate limit (see `clientIp` of the client). */
|
|
46
|
+
clientIp?: (request: Request) => string | null | undefined;
|
|
47
|
+
/** Capabilities source, for tests or a shared cache (default `loadAuthCapabilities`). */
|
|
48
|
+
capabilities?: (sf: StorefrontClient) => Promise<CustomerCapabilities>;
|
|
49
|
+
};
|
|
50
|
+
type AuthActionArgs = {
|
|
51
|
+
request: Request;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The account action: one `POST` form with an `intent` field.
|
|
55
|
+
*
|
|
56
|
+
* | intent | fields | result |
|
|
57
|
+
* | ------------- | ---------------------------------------- | -------------------------------------------------------- |
|
|
58
|
+
* | `login` | email, password | signs in -> redirect |
|
|
59
|
+
* | `register` | email, password, first_name?, last_name? | `step: 'verify'` or, without verification, redirect |
|
|
60
|
+
* | `verify` | email, code | account ready -> redirect |
|
|
61
|
+
* | `otp-request` | email | `step: 'otp'` (code sent) |
|
|
62
|
+
* | `otp-login` | email, code | signs in -> redirect |
|
|
63
|
+
* | `forgot` | email | `step: 'reset'` (always, so it never says who has an account) |
|
|
64
|
+
* | `reset` | email, code, password | new password and signed in -> redirect |
|
|
65
|
+
*
|
|
66
|
+
* Success is a `303` to `next` (validated by `safeNext`) with `Set-Cookie` holding the session token; anything else is
|
|
67
|
+
* an `AuthActionData` to render with `<AccountAuth data={...}>`. Passwords and codes are never echoed back.
|
|
68
|
+
*/
|
|
69
|
+
declare function createAuthAction(options: CreateAuthActionOptions): ({ request }: AuthActionArgs) => Promise<Response | AuthActionData>;
|
|
70
|
+
type AuthAction = ReturnType<typeof createAuthAction>;
|
|
71
|
+
|
|
72
|
+
export { type AuthAction, type AuthActionArgs, AuthActionData, type AuthCookie, type CreateAuthActionOptions, type LoadAuthCapabilitiesOptions, SAFE_AUTH_CAPABILITIES, createAuthAction, loadAuthCapabilities, safeNextPath };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isStorefrontError } from '@asteby/storefront';
|
|
2
|
+
|
|
3
|
+
// src/account/server.ts
|
|
4
|
+
var SAFE_AUTH_CAPABILITIES = Object.freeze({
|
|
5
|
+
password: true,
|
|
6
|
+
email_verification: false,
|
|
7
|
+
otp_channels: Object.freeze([])
|
|
8
|
+
});
|
|
9
|
+
var FAILURE_TTL_MS = 5e3;
|
|
10
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
async function loadAuthCapabilities(sf, options = {}) {
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
const ttl = options.ttlMs ?? 6e4;
|
|
14
|
+
const hit = cache.get(sf);
|
|
15
|
+
if (hit && now() - hit.at < (hit.ok ? ttl : FAILURE_TTL_MS)) return hit.value;
|
|
16
|
+
try {
|
|
17
|
+
const got = await sf.customer.capabilities(options.signal ? { signal: options.signal } : void 0);
|
|
18
|
+
const value = {
|
|
19
|
+
password: got?.password !== false,
|
|
20
|
+
email_verification: got?.email_verification === true,
|
|
21
|
+
otp_channels: Array.isArray(got?.otp_channels) ? got.otp_channels : []
|
|
22
|
+
};
|
|
23
|
+
cache.set(sf, { at: now(), ok: true, value });
|
|
24
|
+
return value;
|
|
25
|
+
} catch {
|
|
26
|
+
cache.set(sf, { at: now(), ok: false, value: SAFE_AUTH_CAPABILITIES });
|
|
27
|
+
return SAFE_AUTH_CAPABILITIES;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function safeNextPath(raw, fallback = "/account") {
|
|
31
|
+
if (typeof raw !== "string") return fallback;
|
|
32
|
+
const v = raw.trim();
|
|
33
|
+
if (!v.startsWith("/") || v.startsWith("//") || v.startsWith("/\\") || /[\u0000-\u001f\\]/.test(v)) return fallback;
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
var fail = (code, extra = {}, field) => ({
|
|
37
|
+
ok: false,
|
|
38
|
+
...extra,
|
|
39
|
+
error: { code, ...field ? { field } : {} }
|
|
40
|
+
});
|
|
41
|
+
function toFailure(err, ctx, extra) {
|
|
42
|
+
if (!isStorefrontError(err)) return fail("unavailable", extra);
|
|
43
|
+
switch (err.status) {
|
|
44
|
+
case 401:
|
|
45
|
+
return fail(ctx === "credentials" ? "invalid_credentials" : ctx === "code" ? "invalid_code" : "unavailable", extra, ctx === "code" ? "code" : void 0);
|
|
46
|
+
case 409:
|
|
47
|
+
return fail("email_taken", extra, "email");
|
|
48
|
+
case 429:
|
|
49
|
+
return fail("rate_limited", extra);
|
|
50
|
+
case 400:
|
|
51
|
+
case 422:
|
|
52
|
+
return fail("validation", extra, err.field);
|
|
53
|
+
case 403:
|
|
54
|
+
case 404:
|
|
55
|
+
return fail("unsupported", extra);
|
|
56
|
+
default:
|
|
57
|
+
return fail("unavailable", extra);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function createAuthAction(options) {
|
|
61
|
+
const { storefront: sf, customerCookie } = options;
|
|
62
|
+
const safeNext = options.safeNext ?? ((raw) => safeNextPath(raw, options.defaultNext ?? "/account"));
|
|
63
|
+
const loadCaps = options.capabilities ?? ((client) => loadAuthCapabilities(client));
|
|
64
|
+
async function signedIn(session, next) {
|
|
65
|
+
const expires = new Date(session.expires_at);
|
|
66
|
+
const cookie = await customerCookie.serialize(session.token, Number.isNaN(expires.getTime()) ? void 0 : { expires });
|
|
67
|
+
return new Response(null, { status: 303, headers: { Location: next, "Set-Cookie": cookie } });
|
|
68
|
+
}
|
|
69
|
+
return async function authAction({ request }) {
|
|
70
|
+
const form = await request.formData().catch(() => null);
|
|
71
|
+
if (!form) return fail("validation");
|
|
72
|
+
const text = (name) => {
|
|
73
|
+
const v = form.get(name);
|
|
74
|
+
return typeof v === "string" ? v : "";
|
|
75
|
+
};
|
|
76
|
+
const intent = text("intent");
|
|
77
|
+
const email = text("email").trim();
|
|
78
|
+
const password = text("password");
|
|
79
|
+
const code = text("code").replace(/\s+/g, "");
|
|
80
|
+
const next = safeNext(text("next") || null);
|
|
81
|
+
const ip = options.clientIp?.(request);
|
|
82
|
+
const w = ip ? { clientIp: ip } : void 0;
|
|
83
|
+
const carry = { email };
|
|
84
|
+
if (!email) return fail("validation", carry, "email");
|
|
85
|
+
const caps = await loadCaps(sf);
|
|
86
|
+
const passwordOn = caps.password;
|
|
87
|
+
const otpOn = caps.otp_channels.includes("email");
|
|
88
|
+
try {
|
|
89
|
+
switch (intent) {
|
|
90
|
+
case "login": {
|
|
91
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
92
|
+
if (!password) return fail("validation", carry, "password");
|
|
93
|
+
return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
|
|
94
|
+
}
|
|
95
|
+
case "register": {
|
|
96
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
97
|
+
if (!password) return fail("validation", carry, "password");
|
|
98
|
+
const res = await sf.customer.register(
|
|
99
|
+
{ email, password, ...text("first_name").trim() ? { first_name: text("first_name").trim() } : {}, ...text("last_name").trim() ? { last_name: text("last_name").trim() } : {} },
|
|
100
|
+
w
|
|
101
|
+
);
|
|
102
|
+
return res.status === "created" ? await signedIn(res.session, next) : { ok: true, step: "verify", email };
|
|
103
|
+
}
|
|
104
|
+
case "verify": {
|
|
105
|
+
if (!code) return fail("validation", carry, "code");
|
|
106
|
+
return await signedIn(await sf.customer.verifyRegistration({ email, code }, w), next);
|
|
107
|
+
}
|
|
108
|
+
case "otp-request": {
|
|
109
|
+
if (!otpOn) return fail("unsupported", carry);
|
|
110
|
+
await sf.customer.requestOtp({ email }, w);
|
|
111
|
+
return { ok: true, step: "otp", email };
|
|
112
|
+
}
|
|
113
|
+
case "otp-login": {
|
|
114
|
+
if (!otpOn) return fail("unsupported", carry);
|
|
115
|
+
if (!code) return fail("validation", carry, "code");
|
|
116
|
+
return await signedIn(await sf.customer.createSession({ email, code }, w), next);
|
|
117
|
+
}
|
|
118
|
+
case "forgot": {
|
|
119
|
+
if (!passwordOn || !otpOn) return fail("unsupported", carry);
|
|
120
|
+
try {
|
|
121
|
+
await sf.customer.requestOtp({ email }, w);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (isStorefrontError(err) && (err.status === 429 || err.status === 400)) throw err;
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, step: "reset", email };
|
|
126
|
+
}
|
|
127
|
+
case "reset": {
|
|
128
|
+
if (!passwordOn) return fail("unsupported", carry);
|
|
129
|
+
if (!code) return fail("validation", carry, "code");
|
|
130
|
+
if (!password) return fail("validation", carry, "password");
|
|
131
|
+
await sf.customer.resetPassword({ email, code, password }, w);
|
|
132
|
+
return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
return fail("validation", carry);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const step = intent === "verify" ? "verify" : intent === "otp-login" ? "otp" : intent === "reset" || intent === "forgot" ? "reset" : void 0;
|
|
139
|
+
const ctx = intent === "login" ? "credentials" : step ? "code" : "other";
|
|
140
|
+
return toFailure(err, ctx, step && intent !== "forgot" ? { ...carry, step } : carry);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { SAFE_AUTH_CAPABILITIES, createAuthAction, loadAuthCapabilities, safeNextPath };
|
|
146
|
+
//# sourceMappingURL=account-server.js.map
|
|
147
|
+
//# sourceMappingURL=account-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/account/server.ts"],"names":[],"mappings":"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","file":"account-server.js","sourcesContent":["/**\n * Server side of the account screen (`@asteby/storefront-react/account/server`): `loadAuthCapabilities` for the loader\n * and `createAuthAction` for the action. No React, no router: Web `Request`/`Response` only, so any framework mounts it.\n *\n * The browser never talks to OPS. The action signs the buyer in with the store's `sk_` client and keeps the session\n * token in the store's own cookie, which is passed in (`customerCookie`), so name, flags and signing stay the store's.\n */\nimport { isStorefrontError, type CustomerCapabilities, type CustomerSession, type StorefrontClient } from '@asteby/storefront'\nimport type { AuthActionData, AuthErrorCode, AuthIntent } from './types.js'\n\nexport type { AuthActionData, AuthErrorCode, AuthIntent, AuthStep } from './types.js'\n\n/** What is assumed when the store cannot be asked: password sign-in works, nothing else is offered. */\nexport const SAFE_AUTH_CAPABILITIES: Readonly<CustomerCapabilities> = Object.freeze({\n password: true,\n email_verification: false,\n otp_channels: Object.freeze([]) as unknown as CustomerCapabilities['otp_channels'],\n})\n\nconst FAILURE_TTL_MS = 5_000\nconst cache = new WeakMap<object, { at: number; ok: boolean; value: CustomerCapabilities }>()\n\nexport type LoadAuthCapabilitiesOptions = {\n /** How long a good answer is reused per client (default 60 s). `0` disables the cache. */\n ttlMs?: number\n signal?: AbortSignal\n /** Clock, for tests. */\n now?: () => number\n}\n\n/**\n * Asks the store which ways of signing in it offers, with a short cache per client. It NEVER throws: when OPS does not\n * answer (older server, outage, token without scope) it returns `SAFE_AUTH_CAPABILITIES` and retries after a few\n * seconds, so the account screen always renders. Use it as the loader: `loader = () => loadAuthCapabilities(storefront)`.\n */\nexport async function loadAuthCapabilities(sf: StorefrontClient, options: LoadAuthCapabilitiesOptions = {}): Promise<CustomerCapabilities> {\n const now = options.now ?? Date.now\n const ttl = options.ttlMs ?? 60_000\n const hit = cache.get(sf)\n if (hit && now() - hit.at < (hit.ok ? ttl : FAILURE_TTL_MS)) return hit.value\n try {\n const got = await sf.customer.capabilities(options.signal ? { signal: options.signal } : undefined)\n const value: CustomerCapabilities = {\n password: got?.password !== false,\n email_verification: got?.email_verification === true,\n otp_channels: Array.isArray(got?.otp_channels) ? got.otp_channels : [],\n }\n cache.set(sf, { at: now(), ok: true, value })\n return value\n } catch {\n cache.set(sf, { at: now(), ok: false, value: SAFE_AUTH_CAPABILITIES })\n return SAFE_AUTH_CAPABILITIES\n }\n}\n\n/** The part of a cookie the action needs. React Router's `createCookie(...)` and Remix's fit as they are. */\nexport type AuthCookie = {\n serialize(value: string, options?: { expires?: Date; maxAge?: number }): string | Promise<string>\n}\n\n/** Allows only a path inside the site (`/account`, `/orders?x=1`); anything else is `fallback`. Blocks `//host` and `/\\host`. */\nexport function safeNextPath(raw: string | null | undefined, fallback = '/account'): string {\n if (typeof raw !== 'string') return fallback\n const v = raw.trim()\n if (!v.startsWith('/') || v.startsWith('//') || v.startsWith('/\\\\') || /[\\u0000-\\u001f\\\\]/.test(v)) return fallback\n return v\n}\n\nexport type CreateAuthActionOptions = {\n storefront: StorefrontClient\n /** The store's cookie for the buyer session token (`createCookie('sf_customer', ...)`). */\n customerCookie: AuthCookie\n /** Validates the `next` field before redirecting (default `safeNextPath`). Return a same-site path. */\n safeNext?: (raw: string | null) => string\n /** Where to go after signing in when there is no `next` (default `/account`). Only used by the default `safeNext`. */\n defaultNext?: string\n /** Shopper IP for the per-shopper rate limit (see `clientIp` of the client). */\n clientIp?: (request: Request) => string | null | undefined\n /** Capabilities source, for tests or a shared cache (default `loadAuthCapabilities`). */\n capabilities?: (sf: StorefrontClient) => Promise<CustomerCapabilities>\n}\n\nexport type AuthActionArgs = { request: Request }\n\nconst fail = (code: AuthErrorCode, extra: Partial<AuthActionData> = {}, field?: string): AuthActionData => ({\n ok: false,\n ...extra,\n error: { code, ...(field ? { field } : {}) },\n})\n\n/** Maps what the SDK threw to the error the form shows. `ctx` tells what a 401 means on this step. */\nfunction toFailure(err: unknown, ctx: 'credentials' | 'code' | 'other', extra: Partial<AuthActionData>): AuthActionData {\n if (!isStorefrontError(err)) return fail('unavailable', extra)\n switch (err.status) {\n case 401:\n return fail(ctx === 'credentials' ? 'invalid_credentials' : ctx === 'code' ? 'invalid_code' : 'unavailable', extra, ctx === 'code' ? 'code' : undefined)\n case 409:\n return fail('email_taken', extra, 'email')\n case 429:\n return fail('rate_limited', extra)\n case 400:\n case 422:\n return fail('validation', extra, err.field)\n case 403:\n case 404:\n return fail('unsupported', extra)\n default:\n return fail('unavailable', extra)\n }\n}\n\n/**\n * The account action: one `POST` form with an `intent` field.\n *\n * | intent | fields | result |\n * | ------------- | ---------------------------------------- | -------------------------------------------------------- |\n * | `login` | email, password | signs in -> redirect |\n * | `register` | email, password, first_name?, last_name? | `step: 'verify'` or, without verification, redirect |\n * | `verify` | email, code | account ready -> redirect |\n * | `otp-request` | email | `step: 'otp'` (code sent) |\n * | `otp-login` | email, code | signs in -> redirect |\n * | `forgot` | email | `step: 'reset'` (always, so it never says who has an account) |\n * | `reset` | email, code, password | new password and signed in -> redirect |\n *\n * Success is a `303` to `next` (validated by `safeNext`) with `Set-Cookie` holding the session token; anything else is\n * an `AuthActionData` to render with `<AccountAuth data={...}>`. Passwords and codes are never echoed back.\n */\nexport function createAuthAction(options: CreateAuthActionOptions) {\n const { storefront: sf, customerCookie } = options\n const safeNext = options.safeNext ?? ((raw: string | null) => safeNextPath(raw, options.defaultNext ?? '/account'))\n const loadCaps = options.capabilities ?? ((client: StorefrontClient) => loadAuthCapabilities(client))\n\n async function signedIn(session: CustomerSession, next: string): Promise<Response> {\n const expires = new Date(session.expires_at)\n const cookie = await customerCookie.serialize(session.token, Number.isNaN(expires.getTime()) ? undefined : { expires })\n return new Response(null, { status: 303, headers: { Location: next, 'Set-Cookie': cookie } })\n }\n\n return async function authAction({ request }: AuthActionArgs): Promise<Response | AuthActionData> {\n const form = await request.formData().catch(() => null)\n if (!form) return fail('validation')\n const text = (name: string) => {\n const v = form.get(name)\n return typeof v === 'string' ? v : ''\n }\n const intent = text('intent') as AuthIntent\n const email = text('email').trim()\n const password = text('password')\n const code = text('code').replace(/\\s+/g, '')\n const next = safeNext(text('next') || null)\n const ip = options.clientIp?.(request)\n const w = ip ? { clientIp: ip } : undefined\n const carry: Partial<AuthActionData> = { email }\n if (!email) return fail('validation', carry, 'email')\n\n const caps = await loadCaps(sf)\n const passwordOn = caps.password\n const otpOn = caps.otp_channels.includes('email')\n\n try {\n switch (intent) {\n case 'login': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!password) return fail('validation', carry, 'password')\n return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next)\n }\n case 'register': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!password) return fail('validation', carry, 'password')\n const res = await sf.customer.register(\n { email, password, ...(text('first_name').trim() ? { first_name: text('first_name').trim() } : {}), ...(text('last_name').trim() ? { last_name: text('last_name').trim() } : {}) },\n w,\n )\n return res.status === 'created' ? await signedIn(res.session, next) : { ok: true, step: 'verify', email }\n }\n case 'verify': {\n if (!code) return fail('validation', carry, 'code')\n return await signedIn(await sf.customer.verifyRegistration({ email, code }, w), next)\n }\n case 'otp-request': {\n if (!otpOn) return fail('unsupported', carry)\n await sf.customer.requestOtp({ email }, w)\n return { ok: true, step: 'otp', email }\n }\n case 'otp-login': {\n if (!otpOn) return fail('unsupported', carry)\n if (!code) return fail('validation', carry, 'code')\n return await signedIn(await sf.customer.createSession({ email, code }, w), next)\n }\n case 'forgot': {\n if (!passwordOn || !otpOn) return fail('unsupported', carry)\n try {\n await sf.customer.requestOtp({ email }, w)\n } catch (err) {\n // Only throttling and a bad email are told; anything else looks the same as success (no account enumeration).\n if (isStorefrontError(err) && (err.status === 429 || err.status === 400)) throw err\n }\n return { ok: true, step: 'reset', email }\n }\n case 'reset': {\n if (!passwordOn) return fail('unsupported', carry)\n if (!code) return fail('validation', carry, 'code')\n if (!password) return fail('validation', carry, 'password')\n await sf.customer.resetPassword({ email, code, password }, w)\n return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next)\n }\n default:\n return fail('validation', carry)\n }\n } catch (err) {\n const step = intent === 'verify' ? 'verify' : intent === 'otp-login' ? 'otp' : intent === 'reset' || intent === 'forgot' ? 'reset' : undefined\n const ctx = intent === 'login' ? 'credentials' : step ? 'code' : 'other'\n return toFailure(err, ctx, step && intent !== 'forgot' ? { ...carry, step } : carry)\n }\n }\n}\n\nexport type AuthAction = ReturnType<typeof createAuthAction>\n"]}
|