Acceso, activación y rate limit
El modelo: 4 capas de control
Sección titulada «El modelo: 4 capas de control»EMA Emi no autentica usuarios finales. Asume que el producto cliente ya autenticó al usuario y que la conversación viene firmada con la API key del producto. El control de acceso vive en cuatro capas:
1. API key (¿qué producto está hablando?)2. App scope (¿qué agente puede invocar este producto?)3. Activación (¿este tenant tiene acceso a esta app?)4. Rate limit (¿pasó el cupo diario de invocaciones?)Capas 3 y 4 son nuevas en Wave 3 y se evalúan en checkAccess.
Capa 1 — API key (autenticación de la app)
Sección titulada «Capa 1 — API key (autenticación de la app)»Cada producto cliente tiene su propia API key. El header
Authorization: Bearer <key> la identifica:
| Secret | Producto | Agente |
|---|---|---|
EMAHEALTH_API_KEY | emahealth.io | sales |
EMACLINIC_API_KEY | emaclinic | support |
EMALAB_API_KEY | emalab | lab_assistant (alpha) |
EMAVAULT_API_KEY | emavault | analytics |
EMAPATH_API_KEY | emapath | pathology_assistant (planned) |
EMAWELL_API_KEY | emawell | wellness_coach (501) |
Sin API key válida → 401.
Capa 2 — App scope (qué agentes/tools)
Sección titulada «Capa 2 — App scope (qué agentes/tools)»permissions.ts → APP_SCOPES define qué puede invocar cada producto.
Mapeo 1:1 entre app y agente. Tools por agente:
| App | Agente | Tools |
|---|---|---|
| emahealth | sales | lead_management |
| emaclinic | support | aidbox per-tenant (11 tools) |
| emavault | analytics | supabase-vault (6 tools) |
| emalab | lab_assistant | (Wave 5+) |
| emapath | pathology_assistant | (Wave 6) |
| emawell | wellness_coach | — (501) |
Si un cliente intenta invocar algo fuera de su whitelist, el middleware rechaza la request.
Capa 3 — Activación per-tenant (Wave 3)
Sección titulada «Capa 3 — Activación per-tenant (Wave 3)»Para apps que requieren tenant (emaclinic, emalab, emapath, emawell), el
header X-Tenant-Id: <uuid> es obligatorio:
POST /api/chatAuthorization: Bearer EMACLINIC_API_KEYX-Tenant-Id: 8c3a2f10-3e5a-4f7e-bc1d-...Si falta → 400 X-Tenant-Id header required.
Para emahealth (sitio público) y emavault (dashboard interno),
X-Tenant-Id es opcional (F002).
Una vez resuelto el tenant, checkAccess consulta emashared.tenant_emi_access:
| Caso | Resultado |
|---|---|
No existe row para el (tenant_id, app) | 403 inactive |
status != 'active' | 403 inactive |
| Supabase query falla | 403 (fail-closed, F003) |
Capa 4 — Rate limit diario
Sección titulada «Capa 4 — Rate limit diario»tenant_emi_access.daily_invocation_limit define cuántas invocaciones
exitosas puede hacer el tenant en un día UTC.
Comportamiento:
- Cuenta
emi_invocationsconstatus='success'yinvoked_atdesde las 00:00 UTC del día actual. - Si la query del count falla → fail-open (F003). No queremos derribar tenants legítimos por una sola query lenta.
- Si supera el límite →
403 rate limited.
Decisión F004: solo invocaciones exitosas cuentan. Deniegos no se penalizan dos veces.
Diferencia entre usuario autenticado vs anónimo
Sección titulada «Diferencia entre usuario autenticado vs anónimo»- SalesAgent (emahealth): el chatbot del website lo invocan
visitantes anónimos. El widget firma con la API key del producto, no
con la sesión del visitante. El
ChatContextpuede traersessionId,visitorEmail,referrerpara tracking. - SupportAgent / AnalyticsAgent / LabAssistant / PathologyAgent:
los productos solo exponen Emi a usuarios logueados. El frontend usa
apiFetchpara incluir el token de Supabase en la request al worker proxy del producto, y ese worker es el que llama a emaemi con la API key correcta +X-Tenant-Id.
Audit log automático
Sección titulada «Audit log automático»Cada request — exitosa, denegada o con error — genera una fila en
emi_invocations. Es fail-silent: un error en audit nunca bloquea
la respuesta al usuario (F006).
Lo que se guarda (sin PHI, F005):
- timestamp, app, agent, tenant_id, status
- turn_count, input_tokens, output_tokens, cost_usd
context_summarycon presencia (has_patient_id, etc.) y metadata no-PHI (action,locale,route)
Lo que nunca se guarda: prompts completos, responses completos, ni ningún valor PHI.