10 KiB
auth
auth es la capa activa de autenticación para Svelte. Su trabajo no es
decidir permisos ni guardar sesiones por su cuenta: su trabajo es reflejar en
cliente el resultado de un motor autoritativo server-side.
El módulo está partido en tres capas, igual que perm y cache:
| Capa | Entrada | Qué contiene |
|---|---|---|
| Lenguaje común | $libs/auth |
Constantes, tipos, errores, eventos, helpers CSRF/token, contratos de adapters |
| Autoridad server | $svrs/auth |
createEngineAuth(), handlers HTTP, password flow, CSRF, devices, session binding |
| Cliente activo | $auth |
createActiveAuth(), estado reactivo, llamadas HTTP, CSRF header wiring |
La regla mental es sencilla:
auth prueba identidad
session mantiene continuidad de sesión
perm decide autorización
cache invalida datos derivados de identidad
storage no guarda secretos
Qué Está Implementado
En esta versión el camino sólido es:
- Password sign-up.
- Password sign-in.
- Current session snapshot.
- Local sign-out.
- Global sign-out.
- CSRF issue/verify.
- Email verification request/complete sobre flows.
- Password reset request/complete sobre flows.
- Device records básicos.
- Eventos/auditoría.
- Invalidación de cache por identidad.
- Typed errors con
code(ErrCodecanónico tipo'auth::credential_invalid'). - Memory/test adapters.
- Adapter DB genérico sin dependencia de ORM.
- Adapter
scryptNode sin dependencia externa. - Contratos para OAuth, MFA y WebAuthn.
OAuth, MFA y WebAuthn existen como superficie de extensión y algunos métodos base, pero no deben documentarse como un flujo production-ready completo aún.
Uso Rápido en Cliente
La forma normal en UI es declararlo desde active-app, porque App ya puede
componer http, cache y session:
import { createActiveApp } from '$active-app';
import { defineActiveAuth, defineEngineHttp } from '$active-app/services';
const App = createActiveApp({
services: {
http: defineEngineHttp({ baseUrl: '' }),
auth: defineActiveAuth({ initial: data.auth })
}
});
const Auth = App.auth;
App.auth existe cuando la app declara el slot auth en services. Igual
que session y perm, solo puede haber un active auth por App:
const Auth = App.auth;
Auth.authenticated;
Auth.loading;
Auth.lastError;
Auth.current.session.status;
Sign-in:
try {
await Auth.signInPassword({
identifier: 'ada@example.com',
password: 'correct horse battery staple'
});
} catch {
// El error seguro queda normalizado en Auth.lastError.
console.log(Auth.lastError?.code);
}
Sign-up:
await Auth.signUpPassword({
identifier: 'ada@example.com',
password: 'correct horse battery staple',
profile: {
displayName: 'Ada Lovelace'
}
});
Logout:
await Auth.signOut();
Logout global:
await Auth.signOutGlobal();
Current session:
await Auth.loadCurrent();
if (Auth.authenticated) {
console.log(Auth.current.actor?.primaryIdentifier);
}
Devices:
const devices = await Auth.listDevices();
await Auth.revokeDevice({
deviceId: devices[0].id
});
Estado Reactivo
ActiveAuth sigue el contrato común ActiveEngine: getters directos,
snapshot(), onChange(), clearError() y dispose().
Auth.current; // AuthCurrentView
Auth.loading; // boolean
Auth.lastError; // AuthClientSafeError | null
Auth.authenticated; // boolean
Auth.mfaRequired; // boolean
Auth.disposed; // boolean
Auth.snapshot(); // AuthCurrentView
En Svelte:
<script lang="ts">
const Auth = App.auth;
</script>
{#if Auth.loading}
<p>Validando...</p>
{:else if Auth.authenticated}
<p>Hola {Auth.current.actor?.displayName}</p>
{:else}
<p>Sesión anónima</p>
{/if}
{#if Auth.lastError}
<p>{Lang.t(codeToLangPath(Auth.lastError.code))}</p>
{/if}
Cómo Funciona CSRF
Las operaciones mutables del active client hacen esto automáticamente:
GET AUTH_ROUTE_PATHS.CSRF- reciben
{ token, expiresAt } - envían el token en
AUTH_HEADER_NAMES.CSRF - el server compara header + cookie firmada
El token no se guarda en storage. Si se pasa storage, solo se marca que CSRF fue
emitido para debugging/UX, pero no se persiste un secreto.
Ejemplo interno equivalente:
const csrf = await http.get(AUTH_ROUTE_PATHS.CSRF);
await http.post(AUTH_ROUTE_PATHS.SIGN_IN_PASSWORD, body, {
headers: {
[AUTH_HEADER_NAMES.CSRF]: csrf.token
}
});
Patrón SvelteKit Recomendado
El server resuelve la sesión y se la pasa al layout o página:
// +layout.server.ts
import { Auth } from '$lib/server/auth';
import { resolveTenantId } from '$lib/server/tenant';
export async function load({ request }) {
const tenantId = resolveTenantId(request);
const auth = await Auth.current({ tenantId, request });
return { auth };
}
En cliente:
<!-- +layout.svelte -->
<script lang="ts">
import { createActiveApp } from '$active-app';
import { defineActiveAuth, defineEngineHttp } from '$active-app/services';
let { data, children } = $props();
const App = createActiveApp({
services: {
http: defineEngineHttp({ baseUrl: '/api' }),
auth: defineActiveAuth({ initial: data.auth })
}
});
const Auth = App.auth;
</script>
{@render children()}
Las rutas HTTP de auth se montan server-side:
// src/web/routes/api/auth/[...path]/+server.ts
import { Auth } from '$lib/server/auth';
import { resolveTenantId } from '$lib/server/tenant';
function input(request: Request) {
return {
request,
tenantId: resolveTenantId(request)
};
}
export const GET = ({ request }) => Auth.handlers.handle(input(request));
export const POST = ({ request }) => Auth.handlers.handle(input(request));
El handler despacha por las rutas constantes:
AUTH_ROUTE_PATHS.CURRENT;
AUTH_ROUTE_PATHS.CSRF;
AUTH_ROUTE_PATHS.SIGN_UP_PASSWORD;
AUTH_ROUTE_PATHS.SIGN_IN_PASSWORD;
AUTH_ROUTE_PATHS.SIGN_OUT;
AUTH_ROUTE_PATHS.SIGN_OUT_GLOBAL;
AUTH_ROUTE_PATHS.EMAIL_VERIFY_REQUEST;
AUTH_ROUTE_PATHS.EMAIL_VERIFY_COMPLETE;
AUTH_ROUTE_PATHS.PASSWORD_RESET_REQUEST;
AUTH_ROUTE_PATHS.PASSWORD_RESET_COMPLETE;
Errores en Cliente
Los errores seguros tienen esta forma:
interface AuthClientSafeError {
code: AuthErrorCode; // ErrCode canónico, p.ej. 'auth::credential_invalid'
}
Ejemplo:
import { AUTH_ERR_CREDENTIAL_INVALID } from '$auth';
try {
await Auth.signInPassword({ identifier, password });
} catch {
const error = Auth.lastError;
if (error?.code === AUTH_ERR_CREDENTIAL_INVALID) {
// Mostrar mensaje traducido desde Lang.
}
}
No uses el texto del error como lógica. Usa los constantes AUTH_ERR_* o
matches(err, AUTH_ERR) para descubrir cualquier error de auth.
Relación con session
auth no emite la cookie principal de sesión. En un login correcto:
authverifica credencial.authcrea/actualiza device.authllama aAuthSessPort.start(...).sessiondevuelvesessionIdy snapshot.authguarda un bindingsessionId -> actorRef.authemite eventos e invalida cache.
En logout:
authlee la sesión desdesession.- revoca el binding auth.
- llama a
session.end(...)osession.endMany(...). - invalida
cache. - emite auditoría.
Relación con perm
auth no decide permisos. Lo único que entrega al resto del sistema es:
actorRef;
sessionId;
aal;
amr;
authTime;
deviceId;
perm puede usar ese contexto para decidir:
Perms.can({
actor,
action: 'project:update',
resource
});
Si cambia la identidad, roles o permisos, el consumer debe invalidar las
decisiones de perm y los datos en cache. Cuando ActiveAuth se declara en
active-app, las reacciones entre auth/cache/session deben cablearse desde
orca/presets o puertos explicitos; auth no debe limpiar caches de forma
oculta.
Relación con storage
No guardes access tokens, refresh tokens, OTPs, CSRF tokens ni passwords en
storage. Si necesitas persistencia de preferencias de auth, guarda solo datos
no sensibles:
storage.entry('last-login-email', '', { raw: true });
Test Page
La página /test/auth monta un harness en memoria y permite probar:
- sign-up
- sign-in
- sign-out
- global sign-out
- reset del harness
- CSRF roundtrip
- CSRF expirado
- snapshot actual
- devices
- credentials
- audit events
- cache invalidations
Es una página de diagnóstico funcional, no un ejemplo de UI final.
Archivos Principales
src/libs/auth/
consts.ts # rutas, headers, cookies, eventos, códigos, defaults
types.ts # tipos públicos
contracts.ts # puertos/adapters
errors.ts # errores tipados + guards
csrf.ts # issue/verify
tokens.ts # random/base64/hash helpers
src/svrs/auth/
engine-auth.ts # createEngineAuth()
handlers.ts # HTTP handlers
options.ts # EngineAuthOptions
adapters/ # memory, db, crypto, password, mail...
integrations/ # bridges con session, perm, cache, logger, timer server-side...
src/arts/auth/
active-auth.svelte.ts
client.ts
types.ts
Qué No Debe Hacer auth
- No guarda perfiles completos de usuario.
- No decide autorización.
- No persiste secretos en cliente.
- No hace magia dentro de
App.http. - No mezcla tenants.
- No autolinka OAuth por email no verificado.
- No expone raw password hashes, tokens o OTPs a UI.
Checklist de Producción
Antes de usarlo fuera de tests:
- Sustituir memory adapters por adapters reales.
- Usar un
AuthStoreAdaptertransaccional. - Conectar
AuthSessPortal módulo realsession. - Conectar el puerto de logger de auth a
logger. - Conectar
AuthCachePortacache. - Usar
createNodeScryptPasswordHasher()o un adapter Argon2id propio. - Resolver
tenantIddesde request, subdominio, organización o app config. - Mantener
security.csrf.signingKeyfuera del repo. - Validar cookies
Secureen producción. - Añadir rate-limit real a sign-in, reset y verification.