Aislamiento multi-tenant Multi-tenant isolation
Todo recurso en AMI vive bajo un account_id explícito. Cada endpoint GET aplica un scope obligatorio antes de leer; el cliente A no puede ver recursos del cliente B aunque conozca su identificador.
Cuando un recurso existe pero pertenece a otra cuenta, devolvemos 404 Not Found en lugar de 403 Forbidden para no filtrar la existencia del recurso por canal lateral.
Hay 29 tests específicos de multi-tenancy que verifican el scoping en cada endpoint de lectura, escritura y listado.
Every resource in AMI lives under an explicit account_id. Each GET endpoint applies a mandatory scope before reading; client A cannot see client B's resources even if A knows the identifier.
When a resource exists but belongs to another account we return 404 Not Found instead of 403 Forbidden so existence is not leaked via side channel.
29 dedicated multi-tenancy tests verify scoping on every read, write and list endpoint.
Niveles de autenticación Authentication levels
Nivel 1 — Customer API key. El cliente humano se autentica con una API key emitida en alta. La key se almacena exclusivamente como SHA-256 hash: nunca podemos restaurarla, solo rotarla.
Nivel 2 — Agent token. Cada agente recibe un agent_token scoped por MID (mobile identity). El token es rotable de forma independiente, por si un agente se compromete sin afectar al resto de la cuenta.
Admin master key. Operaciones de administración (gestión de cuentas, kill-switch) usan una master key separada, fuera del flujo de cliente, con auditoría obligatoria.
Level 1 — Customer API key. The human client authenticates with an API key issued at signup. The key is stored only as a SHA-256 hash: we can never restore it, only rotate it.
Level 2 — Agent token. Each agent receives an agent_token scoped to a single MID (mobile identity). The token rotates independently so a compromised agent does not blast-radius the rest of the account.
Admin master key. Administrative operations (account management, kill-switch) use a separate master key outside the customer flow, with mandatory audit trail.
Webhooks salientes Outbound webhooks
Firmamos cada entrega con HMAC-SHA256 sobre timestamp + body. El receptor verifica la firma y rechaza payloads con timestamp fuera de una ventana de 5 minutos, lo que neutraliza replay attacks.
Antes de cada conexión saliente pasamos la URL por un guard SSRF que resuelve DNS y rechaza cualquier IP en rangos loopback, private, link-local, multicast o reserved — incluyendo el endpoint de metadata cloud 169.254.169.254.
El cliente HTTP no sigue redirects 3xx, y el body de respuesta se trunca a 64 KiB. Así un endpoint malicioso no nos arrastra ni a un host interno ni a un buffer overflow de memoria.
We sign every delivery with HMAC-SHA256 over timestamp + body. The receiver verifies the signature and rejects payloads whose timestamp is outside a 5-minute window, which neutralises replay attacks.
Before each outbound connection the URL goes through an SSRF guard that resolves DNS and rejects any IP in loopback, private, link-local, multicast or reserved ranges — including the cloud metadata endpoint 169.254.169.254.
The HTTP client does not follow 3xx redirects and the response body is capped at 64 KiB. A malicious endpoint cannot drag us into an internal host or a memory blow-up.
Rate limits y spending Rate limits and spending
Cada MID lleva su propio enforcement: rate limit por segundo, presupuesto mensual y allowlist de países destino. Los chequeos se aplican antes de cursar la operación al telco, no después; un agente que se descontrola no llega a generar coste real.
Cuando un límite salta, devolvemos 429 Too Many Requests con una razón específica (rate_per_second, monthly_budget, country_not_allowed) para que el agente pueda decidir en consecuencia sin reintentar a ciegas.
Each MID carries its own enforcement: per-second rate limit, monthly spending budget and destination-country allowlist. Checks run before we forward the operation to the telco, not after; a runaway agent never generates real cost.
When a limit fires we return 429 Too Many Requests with a specific reason (rate_per_second, monthly_budget, country_not_allowed) so the agent can act on it instead of retrying blindly.
Datos en reposo Data at rest
La base de datos SQLite vive en un fichero con permisos 0o600 (lectura/escritura solo del owner). En Render, el filesystem persistente está aislado a la instancia y no es accesible desde otros servicios.
Los secretos de webhook se guardan en plano porque hacen falta para firmar el HMAC saliente — no hay alternativa criptográficamente honesta. La mitigación está en el perímetro: solo el owner del proceso lee el fichero.
The SQLite database lives in a file with 0o600 permissions (read/write only by owner). On Render, the persistent filesystem is isolated to the instance and is not reachable from other services.
Webhook secrets are stored in plaintext because they are needed to sign the outbound HMAC — there is no cryptographically honest alternative. Mitigation lives at the perimeter: only the process owner reads the file.
Audit log Audit log
Cada evento relevante (alta, rotación de key, SMS enviado, llamada, webhook entregado o fallido, kill-switch) queda persistido en una tabla append-only.
El endpoint GET /v1/events filtra por tenant de forma obligatoria — un cliente solo ve sus propios eventos. Los payloads del log llevan lo mínimo para reconstruir lo ocurrido y omiten PII completa: nunca logueamos el cuerpo de un SMS ni el audio de una llamada.
Every relevant event (signup, key rotation, SMS sent, call, webhook delivered or failed, kill-switch) is persisted to an append-only table.
The endpoint GET /v1/events filters by tenant by design — a client only sees its own events. Log payloads carry the minimum needed to reconstruct what happened and omit full PII: we never log the body of an SMS or the audio of a call.
Brand check en CI Brand check enforced in CI
Un workflow de CI hace grep en cada PR sobre las superficies públicas (API, docs, README, partnership). Si entra una marca comercial prohibida, el job falla y bloquea el merge.
Es una regla del socio: AMI se describe por lo que es, no por contra-marcas. La automatizamos para que ningún humano tenga que recordarla en cada cambio.
A CI workflow greps each PR against public surfaces (API, docs, README, partnership). If a prohibited commercial brand slips in, the job fails and blocks merge.
It is a partner rule: AMI is described by what it is, not against other brands. We automate it so no human has to remember on every change.
Tests de regresión Regression tests
305 tests verdes en cada commit, de los cuales 29 son específicos de seguridad: scoping multi-tenant, SSRF, header injection, validación de entrada, DoS por payload, y los residuales de la auditoría externa.
Si una regresión rompe cualquiera de los 29, el merge se bloquea. La seguridad no depende de la disciplina de un revisor humano.
305 green tests on every commit, of which 29 are dedicated to security: multi-tenant scoping, SSRF, header injection, input validation, payload DoS, and the residuals from the external audit.
If a regression breaks any of the 29, merge is blocked. Security does not depend on a human reviewer's discipline.