// Cuentas de cliente reales conectadas a Medusa (Store API), mismo patron // que medusa-cart.ts -- server-side, nunca cacheado (auth es estado // mutable por sesion, no catalogo). // // Flujo verificado en vivo (2026-08-29) antes de escribir este archivo, // no asumido contra documentacion: registro (auth/customer/emailpass/ // register) da un token de registro; ese token crea el customer real // (POST /store/customers); pero ese token de registro NO sirve como // sesion del customer -- hay que loguear de nuevo para obtener el token // de sesion real. Mensajes de error exactos confirmados: registro con // correo repetido -> 401 "Identity with email already exists"; login con // credenciales invalidas -> 401 "Invalid email or password". export interface CustomerSummary { token: string; id: string; email: string; firstName: string | null; lastName: string | null; } function backendUrl(): string { const value = process.env.MEDUSA_BACKEND_URL; if (!value) { throw new Error("MEDUSA_BACKEND_URL no está configurado."); } return value.replace(/\/$/, ""); } async function medusaAuthFetch( path: string, init?: { method?: string; body?: unknown; token?: string }, ): Promise> { const publishableKey = process.env.MEDUSA_PUBLISHABLE_KEY; const response = await fetch(`${backendUrl()}${path}`, { method: init?.method ?? "GET", headers: { "Content-Type": "application/json", ...(publishableKey ? { "x-publishable-api-key": publishableKey } : {}), ...(init?.token ? { Authorization: `Bearer ${init.token}` } : {}), }, body: init?.body ? JSON.stringify(init.body) : undefined, cache: "no-store", }); const data = await response.json().catch(() => ({})); if (!response.ok) { const message = typeof data.message === "string" ? data.message : `Error ${response.status}`; throw new Error(message); } return data; } type MedusaCustomer = { id: string; email: string; first_name?: string | null; last_name?: string | null; }; export async function getCurrentCustomer(token: string): Promise { const data = await medusaAuthFetch("/store/customers/me", { token }); const customer = data.customer as MedusaCustomer; return { token, id: customer.id, email: customer.email, firstName: customer.first_name ?? null, lastName: customer.last_name ?? null, }; } export async function loginCustomer(email: string, password: string): Promise { let data: Record; try { data = await medusaAuthFetch("/auth/customer/emailpass", { method: "POST", body: { email, password }, }); } catch (error) { if ((error as Error).message === "Invalid email or password") { throw new Error("Correo o contraseña incorrectos."); } throw error; } return getCurrentCustomer(data.token as string); } // Flujo Google verificado en vivo (2026-08-29) contra el pod real, no // solo contra la doc: GET /auth/customer/google funciona igual que el // POST documentado (sin body, no hace falta callback_url -- ya viene de // GOOGLE_CALLBACK_URL en el modulo); la respuesta no trae Set-Cookie, o // sea que el "state" que Google reenvia en el callback es autocontenido // (firmado), no depende de sesion de servidor -- confirmado con un // callback con code/state inventados: 401 "No state provided, or // session expired" (mismo mensaje que con state ausente, no un error de // sesion real). auth/token/refresh confirmado que exige Bearer valido // (401 "Unauthorized" con uno invalido, no 404). function decodeJwtPayload(token: string): Record { const payload = token.split(".")[1] ?? ""; const base64 = payload.replace(/-/g, "+").replace(/_/g, "/"); const json = Buffer.from(base64, "base64").toString("utf-8"); return JSON.parse(json || "{}"); } export async function getGoogleAuthUrl(): Promise { const data = await medusaAuthFetch("/auth/customer/google"); return data.location as string; } export async function handleGoogleCallback(code: string, state: string): Promise { const query = new URLSearchParams({ code, state }).toString(); const data = await medusaAuthFetch(`/auth/customer/google/callback?${query}`, { method: "POST" }); const initialToken = data.token as string; const payload = decodeJwtPayload(initialToken); const actorId = payload.actor_id as string | undefined; if (!actorId) { // Primer login con Google -- todavia no existe el customer real. const userMetadata = (payload.user_metadata as Record | undefined) ?? {}; const email = userMetadata.email as string; const firstName = (userMetadata.given_name as string) ?? (userMetadata.name as string) ?? email.split("@")[0]; const lastName = (userMetadata.family_name as string) ?? ""; await medusaAuthFetch("/store/customers", { method: "POST", token: initialToken, body: { email, first_name: firstName, last_name: lastName }, }); } // El token del callback (o el de recien crear el customer) todavia no // sirve como sesion final -- hay que refrescarlo, tanto para usuarios // nuevos como para los que ya existian (confirmado contra la doc: los // dos casos convergen en el mismo paso de refresh). const refreshed = await medusaAuthFetch("/auth/token/refresh", { method: "POST", token: initialToken }); return getCurrentCustomer(refreshed.token as string); } export async function registerCustomer( email: string, password: string, firstName: string, lastName: string, ): Promise { let data: Record; try { data = await medusaAuthFetch("/auth/customer/emailpass/register", { method: "POST", body: { email, password }, }); } catch (error) { if ((error as Error).message === "Identity with email already exists") { throw new Error("Ese correo ya está registrado — inicia sesión en vez de crear una cuenta nueva."); } throw error; } await medusaAuthFetch("/store/customers", { method: "POST", token: data.token as string, body: { email, first_name: firstName, last_name: lastName }, }); // El token de registro crea el customer pero no queda como sesion real // -- se loguea de nuevo para obtener el token de sesion (confirmado en // vivo, no documentado explicitamente). return loginCustomer(email, password); }