Middleware
La API encadena 15 middlewares (web/api/src/middleware/) que cubren seguridad de
cabeceras, CORS, CSRF, resolución de tenant, autenticación, RBAC, fechas UTC, auditoría y
manejo de errores. El orden de registro en app.ts es parte del diseño de seguridad.
Orden de ejecución (app.ts)
Request
│ helmet → cabeceras (HSTS 1 año + preload, frameguard deny, no-CSP)
│ cors → whitelist localhost + CORS_ORIGINS + regex *.zentto.net, credentials
│ webhooks RAW → /api/webhooks, /v1/billing/webhook (firma sobre body crudo,
│ ANTES de express.json)
│ express.json → límite 2 MB
│ observability → @zentto/obs (cada request; >1s = perf, 5xx = error) → Loki
│ csrf-origin → valida Origin/Referer en POST/PUT/PATCH/DELETE
│ subdomain-tenant → Origin → slug → CompanyId (req._tenantCompanyId)
│ [rutas públicas] → /health, /store, /shipping, /v1/registro, ...
│ requireJwt (/v1) → cookie zentto_token o Bearer; scope de empresa; pool del tenant
│ autoRbac (/v1) → permiso por módulo (ruta) + acción (método HTTP)
│ datetime (/v1) → entrada → UTC · salida → timezone de la empresa
│ audit-trail (/v1) → mutaciones exitosas → audit.AuditLog + obs
│ [routers de negocio]
▼ globalErrorHandler → SIEMPRE el último
Response Regla crítica de orden
Un router montado antes de app.use('/v1', requireJwt) queda sin
autenticación (bug real ya vivido). Los webhooks raw van antes de express.json
porque validan firma sobre el body crudo.
auth.ts — autenticación
Exporta requireJwt, requireAdmin y
requireBackofficeFederatedJwt. El JWT llega por cookie httpOnly
zentto_token (navegador) o header Authorization: Bearer
(server-to-server / móvil). Verificación en dos vías:
- JWKS de zentto-auth (RS256,
AUTH_SERVICE_URL/.well-known/jwks.json) — los tokens emitidos por el broker central de identidad. - HS256 local con
JWT_SECRET(+JWT_SECRET_FALLBACKopcional, solo durante rotaciones de secret — se aplica únicamente ante invalid signature, no ante expiración).
Tras validar, resuelve el scope de empresa (claim companyAccesses,
recortado en el token porque las cookies >4 KB se descartan — el superadmin resuelve su scope
real en servidor) y arranca el contexto del request (AsyncLocalStorage) con el
pool del tenant, que es lo que callSp() consume después.
La verificación de contraseñas es bcrypt en Node, nunca en SQL.
subdomain-tenant.ts — multi-tenant por subdominio
Corre antes del JWT. Extrae el hostname del Origin/Referer;
los dominios conocidos del ecosistema hacen bypass; un slug de tenant se resuelve contra la BD
master (cache 5 min + cache negativo) e inyecta req._tenantCompanyId. Slug no
registrado → 404 tenant_not_found, nunca fallback a otra BD.
csrf-origin.ts — defensa CSRF
Con auth por cookie, SameSite solo no basta: este middleware valida que el
Origin/Referer de toda mutación (POST/PUT/PATCH/DELETE) pertenezca al
ecosistema. Los llamados server-to-server con Bearer pasan sin Origin.
rbac.ts — permisos granulares
autoRbac deriva el módulo desde la ruta y la acción desde el método HTTP, y consulta
usp_Sec_UserPermission_Check. Modo por RBAC_MODE:
off · log (default — registra sin bloquear) · enforce.
datetime.ts — UTC-0 a fuego
normalizeRequestDateTimesToUtc convierte las fechas del request a UTC;
localizeResponseDateTimes convierte la salida al timezone de la empresa
(no del navegador). La BD almacena exclusivamente UTC.
error-handler.ts — manejo global (ALERT-3)
// Patrón canónico en routes:
try { ... } catch (err) { next(err); }
// o
throw new ApiError(400, 'invalid_payload', 'detalle para el usuario');
El handler global reconoce ApiError, errores de parseo JSON y Zod; responde siempre
JSON con código estable y nunca expone stack en producción. Se registra al final
de createApp(). Prohibido res.status(500).json({ error: String(err) }).
Los demás middlewares
| Archivo | Responsabilidad |
|---|---|
| observability.ts | Instrumentación de cada request con @zentto/obs v2 → Loki
(requests lentos como perf, 5xx como error) |
| audit-trail.ts | Mutaciones exitosas → audit.AuditLog + evento de auditoría
(best-effort, no bloquea la respuesta) |
| master-key.ts | Backoffice: X-Backoffice-Token (JWT post-TOTP) o
X-Master-Key (compat CI) |
| service-token.ts | Auth server-to-server: Bearer zst_<random>; en BD solo
el hash SHA-256 (cfg."ServiceToken") |
| erp-identity.ts | Mapea el sub (UUID de zentto-auth) a la fila
sec.User del ERP por empresa (trazabilidad) |
| superadmin-scope.ts | Resuelve en servidor el scope real de un superadmin (el claim viaja capado por el límite de 4 KB de cookie) |
| subscription.ts | Verificación de suscripción por usuario — se valida en el login, no por request |
| rate-limit.ts | Rate limiting con Redis ({name, max, windowSec}) |
| backoffice-session-secret.ts | Lee credenciales operativas en tiempo de uso (Vault carga async — nunca capturar secretos en constantes de módulo al importar) |
CORS en producción
Express valida el origen (whitelist + regex *.zentto.net, credentials: true);
Nginx añade los headers CORS con flag always para que lleguen incluso en 502/500, y
oculta los del upstream para evitar duplicados. Preflight OPTIONS se responde en Nginx.