@asteby/storefront-react 0.12.0 → 0.13.1
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 -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 +74 -0
- package/dist/account.d.ts +74 -0
- package/dist/account.js +211 -0
- package/dist/account.js.map +1 -0
- package/dist/checkout.cjs +12 -2
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +5 -100
- package/dist/checkout.d.ts +5 -100
- package/dist/checkout.js +17 -233
- package/dist/checkout.js.map +1 -1
- package/dist/chunk-DFGLCIEG.js +226 -0
- package/dist/chunk-DFGLCIEG.js.map +1 -0
- package/dist/{chunk-ODIQR6TG.js → chunk-JNUJGPJC.js} +14 -4
- package/dist/chunk-JNUJGPJC.js.map +1 -0
- package/dist/fields-vnMUi2HB.d.cts +101 -0
- package/dist/fields-vnMUi2HB.d.ts +101 -0
- package/dist/payments.cjs +12 -2
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.js +2 -2
- 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
|
@@ -668,3 +668,36 @@ y define los tokens `--background --foreground --border --tint --primary --prima
|
|
|
668
668
|
| `ThemeToggle`, `THEME_INIT_SCRIPT` | Claro/oscuro con `html[data-theme]` y localStorage, sin parpadeo. |
|
|
669
669
|
| `InstallApp`, `useServiceWorker` | Instalación PWA (Android/Chrome y pista para iOS) y registro del service worker. |
|
|
670
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"]}
|