# Tenant Context ## Objetivo Documentar cómo se resuelve el contexto tenant actual del ERP mediante `X-Company-Id` y `requireIdentityTenant(params)`. ## Header tenant Las rutas tenant-scoped deben recibir: ```http Authorization: Bearer X-Company-Id: ``` `X-Company-Id` es el identificador de la company activa en la request. No usar: - `X-Company-Slug` - `companySlug` en token - `req.user.companySlug` ## Middleware público Para rutas tenant-scoped: ```ts import { requireIdentityTenant } from "@erp/identity/api"; router.use(...requireIdentityTenant(params)); ``` Para rutas solo autenticadas: ```ts import { requireIdentityAuthenticated } from "@erp/identity/api"; router.use(...requireIdentityAuthenticated(params)); ``` ## Validación real de `requireIdentityTenant(params)` Actualmente ya valida: 1. access token válido 2. account autenticable 3. `X-Company-Id` presente 4. `X-Company-Id` con formato UUID válido 5. membership `active` en `identity_company_memberships` o titularidad global de Super-administrador 6. company `active` en `companies` 7. solo entonces rellena `req.user.companyId` Esto significa que hoy el contexto tenant garantiza: ```txt (membership activa o Super-administrador activo) + company activa ``` La autorización granular se aplica después, mediante `requirePermission(authorization, code)`. El tenant por sí solo no concede permisos de operación. ## Semántica esperada de errores ```txt sin Authorization -> 401 token inválido -> 401 cuenta inexistente/no autenticable -> 401 sin X-Company-Id -> 403 X-Company-Id inválido -> 400 company inexistente -> 403 company disabled -> 403 membership inexistente en una cuenta ordinaria -> 403 membership disabled/invited en una cuenta ordinaria -> 403 membership active + company active -> entra al controller ``` ## Shape mínimo de `req.user` ```ts { userId: UniqueID; email?: EmailAddress; companyId?: UniqueID; roles?: string[]; } ``` No añadir: - `companySlug` - `company` - `permissions` enriquecidos - `companyStatus` ## Módulos tenant-scoped migrados Actualmente usan `requireIdentityTenant(params)`: - `modules/companies` para `GET /companies/current`, `PATCH /companies/current` y endpoints company-scoped legacy - `modules/catalogs` - `modules/customers` - `modules/customer-invoices` - `modules/factuges` `modules/suppliers` también usa el contexto tenant canónico. ## Qué no debe ir en el token El access token no debe incluir: - `companyId` - `companySlug` - roles - permisos El contexto tenant se resuelve por request con `X-Company-Id`. ## Caso actual: company settings `/settings/company/details` opera siempre sobre la company activa. Endpoints: ```http GET /companies/current PATCH /companies/current ``` Política actual de lectura `GET /companies/current`: autenticación, membership y empresa activas. No se añade un permiso a esta consulta usada por selección/configuración actual. La edición `PATCH /companies/current` y `PATCH /companies/:company_id`, así como `POST /companies/:company_id/enable` y `/disable`, requieren además `identity:companies:update`. Antes bastaba el contexto empresarial. `GET /companies/:company_id` requiere `companies:companies:read`. Antes bastaba el tenant. El parámetro sigue comparándose con la empresa autenticada; el permiso de A no autoriza B. Las operaciones globales de cuenta y consulta de empresas disponibles siguen autenticadas. `POST /companies` exige `companies:companies:create` (SYSTEM); su transacción crea la empresa y asigna al actor su administrador inicial con todos los COMPANY actuales. Los POST de onboarding inicial exigen `identity:system:initial-onboarding` (SYSTEM). La lectura de estado del onboarding sigue autenticada. No se introduce autorización granular en todas las operaciones de todos los módulos de una vez. ## Document context Si un flujo documental necesita `companySlug`, debe resolverlo desde `companies`, por ejemplo: ```txt companyId -> companies:general.finder.findById(...) -> slug ``` ## Autorización y registro de permisos La raíz de cada módulo declara `permissions: PermissionDefinition[]` en su manifest backend. El runtime compone y valida códigos, namespace y duplicados **antes de cualquier setup**. Cada definición declara `scope: "SYSTEM" | "COMPANY"`; un scope ausente o inválido impide el setup. Registrar un COMPANY amplía el rol reservado Administradores durante el mantenimiento de arranque y en nuevas altas. Los demás roles conservan sus permisos explícitos. Identity no importa los módulos contribuyentes. La raíz del consumidor resuelve `identity:general` y suministra `authorization` a sus rutas. La API pública neutral es `authorization.can({ accountId, companyId, permissionCode, transaction? })`, que devuelve `Result`. Código desconocido o no asignado deniega; fallos técnicos no conceden. El guard `requirePermission(authorization, code)` se instala después de auth/tenant y solo toma cuenta/empresa de `req.user`. Falta de autenticación devuelve 401; tenant/permiso ausente 403; fallo técnico 500 con mensaje seguro. No depende de permisos frontend. `CompanyAdminRoleEnsurer` obtiene todos los COMPANY del catálogo completo suministrado por el runtime. Identifica el rol reservado por `code = admin` e `isSystem = true`, con nombre de alta `Administradores`. Añade los códigos ausentes, conserva adicionales y fechas cuando no cambia nada, y nunca incorpora SYSTEM. El nombre histórico `Administrator` puede conservarse en filas existentes: no identifica privilegios. Los roles normales no se modifican. Poseer un código SYSTEM dentro de un JSON de rol empresarial tampoco concede autorización global: `canSystem` solo consulta la capacidad global persistida. El warmup obligatorio de Identity enumera únicamente roles `admin` reservados, los relee con bloqueo y aplica la unión en una transacción por empresa. Se ejecuta tras preparar el esquema y antes de servir HTTP; si falla o expira, impide arrancar, aunque los warmups opcionales no sean estrictos. Se puede repetir. No hay escrituras al resolver permisos ni sincronización durante requests. ### Super-administrador y bootstrap Identity persiste una titularidad global en `identity_system_owner`, fila `id = 1`, vinculada a una cuenta activa. No se deriva de email, nombre, fecha de alta ni rol empresarial. `systemBootstrap.create(props)` es la entrada confiable de composición que crea la primera cuenta y reserva la titularidad en la misma transacción: bloquea el singleton y rechaza un almacén que ya contiene cuentas. No existe un endpoint público de promoción. El bootstrap recibe Value Objects, incluido el hash generado por el hasher de Identity; no recibe contraseñas por HTTP. El usuario creado puede autenticarse y completar el onboarding inicial existente. La cuenta global dispone de todos los permisos registrados según su scope, también los futuros. `authorization.canSystem({ accountId, permissionCode, transaction? })` exige SYSTEM y no requiere tenant. `authorization.can({ accountId, companyId, permissionCode, transaction? })` exige COMPANY y se invoca después de validar el tenant; el permiso global no permite omitir `X-Company-Id` ni usar una empresa inexistente/inactiva. El selector de empresas disponibles incluye todas las activas para el Super-administrador. Las cuentas ordinarias reciben únicamente sus empresas con membership activa. Los catálogos que usan `requireIdentityTenantMembership` también comprueban empresa activa: el host suministra `tenantCompanyFinder` durante `startModules`, una vez construidos los módulos. Usa el finder público de Companies, sin imports de su infraestructura ni dependencia circular de setup Catalogs → Companies → Catalogs. Un host que no suministre ese puerto no puede iniciar esas rutas. El helper conserva su nombre por compatibilidad. ### Bases existentes Con `DB_SYNC_MODE=none`, aplicar expresamente `modules/identity/sql/001-system-owner.sql` antes de iniciar esta versión. La DDL es idempotente y no promueve cuentas. En una base existente debe identificarse y acreditarse la cuenta inicial antes de establecer expresamente su titularidad mediante un procedimiento administrativo revisado. No se ha ejecutado esa asignación ni ninguna DDL sobre datos reales. No se elige automáticamente al primer admin ni a la cuenta más antigua. Los roles Administradores existentes sí reciben los COMPANY en el arranque, conservando sus permisos adicionales. Patrón de composición (referencia real: `companies.di.ts` y `companies.routes.ts`): ```ts // Composition root: resolver una sola vez y suministrar el contrato público al router. const identity = params.getService("identity:general"); // Route, después de autenticación y tenant: router.patch("/current", tenant, requirePermission(identity.authorization, permissionCode), handler); ``` No reconstruir resolver, roles o repositories de Identity desde otro módulo.