You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/auth/README.md

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 (ErrCode canónico tipo 'auth::credential_invalid').
  • Memory/test adapters.
  • Adapter DB genérico sin dependencia de ORM.
  • Adapter scrypt Node 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:

  1. GET AUTH_ROUTE_PATHS.CSRF
  2. reciben { token, expiresAt }
  3. envían el token en AUTH_HEADER_NAMES.CSRF
  4. 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:

  1. auth verifica credencial.
  2. auth crea/actualiza device.
  3. auth llama a AuthSessPort.start(...).
  4. session devuelve sessionId y snapshot.
  5. auth guarda un binding sessionId -> actorRef.
  6. auth emite eventos e invalida cache.

En logout:

  1. auth lee la sesión desde session.
  2. revoca el binding auth.
  3. llama a session.end(...) o session.endMany(...).
  4. invalida cache.
  5. 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 AuthStoreAdapter transaccional.
  • Conectar AuthSessPort al módulo real session.
  • Conectar el puerto de logger de auth a logger.
  • Conectar AuthCachePort a cache.
  • Usar createNodeScryptPasswordHasher() o un adapter Argon2id propio.
  • Resolver tenantId desde request, subdominio, organización o app config.
  • Mantener security.csrf.signingKey fuera del repo.
  • Validar cookies Secure en producción.
  • Añadir rate-limit real a sign-in, reset y verification.

Powered by TurnKey Linux.