Desenvolvedores
Autenticação do cliente
A conta do cliente final (perfil, endereços, pedidos) usa uma sessão própria em cookies httpOnly: nenhum token transita pelo seu JavaScript. Não existe chave de API: rotas públicas não exigem nada, rotas de conta exigem a sessão do próprio cliente.
Sessão em dois cookies
customer_session: JWT de acesso, expira em ~15 minutos.customer_refresh: refresh rotativo, ~30 dias; cada uso emite um novo par.
curl -c cookies.txt -X POST https://SUA-LOJA.meweb.com.br/api/v1/storefront/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "cliente@example.com", "password": "..." }'
# Set-Cookie: customer_session=... (JWT, ~15 min)
# Set-Cookie: customer_refresh=... (rotativo, ~30 dias)# Em 401 CUSTOMER_UNAUTHENTICATED: renove UMA vez e repita a requisição original
curl -b cookies.txt -c cookies.txt -X POST \
https://SUA-LOJA.meweb.com.br/api/v1/storefront/auth/refresh
# → { "data": { "ok": true } } (ok: false = refresh expirado → tela de login)Padrão do tema oficial: em 401, chamar refresh uma única vez (deduplicando chamadas concorrentes) e repetir a requisição. A sessão é presa à loja: o token de uma loja não vale em outra.
Por que um proxy BFF
Os cookies são sameSite: lax e pertencem ao domínio da API; um front em outro domínio não consegue enviá-los em chamadas de browser (fetch cross-site). Servindo a API pelo seu próprio domínio (rota /api/* que repassa para a loja), os cookies ficam same-origin e tudo (carrinho, sessão, conta) funciona como no tema oficial:
// app/api/[...path]/route.ts (Next.js): proxy BFF resumido
export async function POST(req: Request) {
const upstream = await fetch(apiUrl(req), {
method: 'POST',
headers: {
'content-type': 'application/json',
cookie: req.headers.get('cookie') ?? '',
'x-forwarded-host': process.env.MEWEB_STORE_HOST!,
},
body: await req.text(),
});
const res = new Response(await upstream.text(), { status: upstream.status });
// Repasse CADA Set-Cookie separadamente (nunca junte com vírgula)
for (const c of upstream.headers.getSetCookie()) res.headers.append('set-cookie', c);
return res;
}- Injete
X-Forwarded-Hostcom o host da loja: é assim que a plataforma resolve o tenant. - Repasse os
Set-Cookieindividualmente (getSetCookie()); login emite múltiplos cookies de uma vez. - Não repasse
content-encoding/content-lengthdo upstream.
O tema starter implementa esse proxy completo; use-o como referência mesmo que seu front não seja Next.js.
Limites
login e register têm limite de 10 req/min por IP. Solicitação de redefinição de senha responde sempre ok (sem revelar se o e-mail existe).
Desafio progressivo no login
O limite por IP não segura uma botnet que rotaciona endereços contra a senha de um cliente. Por isso, depois de 5 falhas do mesmo e-mail em 15 minutos, a conta entra em modo desafio por 15 minutos (um login correto zera o contador). No fluxo normal nada é exigido: implemente o desafio sob demanda, disparado pelo error.code:
422 CAPTCHA_REQUIREDsignifica que a loja está sendo servida em um subdomíniomeweb.com.br: renderize o widget do Cloudflare Turnstile e repita o login comturnstileToken. O token é de uso único: peça um novo a cada falha, senão a resposta vira422 CAPTCHA_FAILEDpara sempre.429 RATE_LIMITED: em domínio próprio do lojista (onde a allowlist do Turnstile não alcança e o widget não renderiza), o desafio é um limite de 1 tentativa a cada 30 s por e-mail; respeite oRetry-Aftere ofereça o link de redefinição de senha.
# Tentativa de login em modo desafio, sem token (subdomínio da plataforma)
# → 422 { "error": { "code": "CAPTCHA_REQUIRED", "message": "Por segurança, confirme que você não é um robô." } }
# Renderize o widget do Cloudflare Turnstile e reenvie COM o token:
curl -c cookies.txt -X POST https://SUA-LOJA.meweb.com.br/api/v1/storefront/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "cliente@example.com", "password": "...", "turnstileToken": "TOKEN_DO_WIDGET" }'
# 422 CAPTCHA_FAILED = token recusado (uso único!) → peça um token NOVO ao widget
# Em domínio próprio, onde o widget do Turnstile não renderiza, o desafio é outro:
# → 429 { "error": { "code": "RATE_LIMITED", ... } } + Retry-After: 30Os dois códigos saem iguais para e-mail inexistente: o desafio não revela se a conta existe. Não trate nenhum deles como "conta bloqueada".