Skip to main content
Traducción. Autoritativo: fs-core-0009-member-identity.md.

Contexto

La suite sirve a dos poblaciones que nunca deben mezclarse: los usuarios que operan un tenant y los members que son sus clientes. Difieren en escala, en confianza y en radio de daño — un token de member que pudiera alcanzar un permiso de consola sería un compromiso total. ADR-010 mantiene a Softcrum como autoridad de identidad mientras deja que las integraciones embebidas conserven su propia experiencia de login. La entrada nativa es OTP o magic link; la delegada es token exchange, donde el backend del tenant firma una aserción de vida corta y recibe un token de member con scopes. Es el patrón que usa Smile.io, y es lo que hace del modo headless un ciudadano de primera clase en vez de una versión degradada. Todo member autenticado mapea 1:1 a una fila de core.contacts. No hay member sin contacto, porque el contacto es aquello de lo que trata el resto de la plataforma.

Alcance (normativo)

  • Un realm de Better Auth separado para members, con sus propias tablas y configuración.
  • Entrada nativa: OTP y magic link por correo, vía Resend.
  • Entrada delegada: POST /v1/core/auth/token-exchange.
  • Llaves de firma por tenant, con rotación.
  • Tokens de member con scopes: member:read, member:redeem, member:referral.
  • Aserciones de un solo uso con verificación de replay por jti en Redis.
  • El mapeo contacto ↔ member.

Fuera de alcance (normativo)

  • La autenticación de usuarios de consola — el realm organizacional, anterior a este módulo.
  • Login social para members. Diferido: agrega superficie de proveedores para una población que llega mayormente por enlace.
  • Login con contraseña para members. Solo OTP y magic link, deliberadamente: sin contraseña de member no hay contraseña de member que se filtre.

Comportamiento (normativo)

  1. Los members viven en un realm separado sin superposición de sesión, cookie ni token con el organizacional. Esto es aislamiento de realm, no una verificación de permisos: la separación es estructural, así que no hay camino de escalamiento de privilegios que equivocar.
  2. Un token de member nunca puede portar un permiso de consola. Impuesto por construcción, y por un test que lo afirma.
  3. Una aserción de token exchange es un JWT firmado con una llave por tenant, válido ≤5 minutos, de un solo uso vía verificación de replay por jti en Redis. Vida corta más uso único significa que una aserción capturada casi no vale nada.
  4. La aserción lleva el external_id del member según el tenant. La resolución encuentra o crea el contacto correspondiente — un member que llega por primera vez no es un error.
  5. Todo member autenticado mapea 1:1 a un contacto. Un member sin contacto es un estado inválido.
  6. Las llaves de firma rotan sin invalidar sesiones vivas: una ventana de solapamiento acepta ambas. Una rotación que desconecta a todos es una rotación que nadie ejecuta.
  7. Los tokens de member tienen scopes. Un token member:read no puede canjear, y el scope se verifica en el endpoint, no en el cliente.
  8. Los códigos OTP son de un solo uso, expiran en 10 minutos y tienen rate limit por contacto y por IP.
  9. PROHIBIDO: registrar en logs un token o una aserción · aceptar una aserción firmada con la llave de otro tenant · emitir un token de member cuyo contacto pertenece a otro tenant.

Datos (normativo)

Better Auth es dueño de las tablas propias del realm; esta registra lo que necesitamos para revocación y auditoría.

API (normativo)

Eventos (normativo)

Ninguno en el outbox. La autenticación se registra en core.audit_log con actor member.

Criterios de aceptación (normativo)

  1. Un token de member es rechazado por todo endpoint de consola — afirmado sobre el conjunto completo, no una muestra.
  2. Una aserción reproducida (mismo jti) se rechaza en el segundo uso.
  3. Una aserción con más de 5 minutos se rechaza.
  4. Una aserción firmada con la llave del tenant A no puede emitir un token para un member del tenant B.
  5. La rotación de llaves mantiene válidas las sesiones existentes durante toda la ventana de solapamiento.
  6. Un external_id visto por primera vez crea el contacto y devuelve un token en una sola llamada.
  7. Los códigos OTP son de un solo uso y expiran.
  8. Negativo: ninguna línea de log contiene un token, una aserción ni un código OTP.

Ejecución

Comando síncrono. Configuración del realm y endpoints en backend/api; la verificación de replay usa las mismas primitivas de Upstash que el rate limiting.

Preguntas abiertas

Changelog

Registro de entrega

Aún no implementado.