Versión: 1.0
Base URL producción: https://api.conectapui.com/api/v1
Base URL sandbox: https://sandbox-api.conectapui.com/api/v1
Región AWS: us-west-2 (Oregon)
Referencia normativa: Manual Técnico PUI v1.0 (DOF 13/01/2026)
La API maneja dos canales de autenticación independientes:
┌─────────────────────────────────────────────────────────────┐
│ CANAL A — Institución → ConectaPUI (Dashboard) │
│ Auth: JWT de AWS Cognito (Bearer token) │
│ Aplica a: /auth/*, /records/*, /uploads/*, /dashboard/*, │
│ /tenants/*, /search/*, /referrals/* │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ CANAL B — Servidor PUI → ConectaPUI (Integración gov.) │
│ Auth: HS256 JWT propio (obtenido en /pui/login) │
│ Aplica a: /pui/* │
│ El "usuario" siempre debe ser exactamente "PUI" (3 chars) │
│ TTL del token: 1 hora │
└─────────────────────────────────────────────────────────────┘
Todas las rutas del dashboard requieren el header:
Authorization: Bearer <cognito-id-token>
El token Cognito contiene los claims custom:tenantId y custom:role.
Se obtiene mediante el flujo estándar de Amplify/Cognito en el frontend.
El servidor de la PUI obtiene un token en dos pasos:
POST /pui/login → recibe { token }Authorization: Bearer <token> en llamadas subsecuentesEl token expira en 1 hora. La PUI debe reautenticar cuando recibe 401.
Estos endpoints son llamados por el servidor de la PUI hacia ConectaPUI. La URL base que debe registrar la PUI es:
https://api.conectapui.com/api/v1/pui
POST /pui/loginAutenticación del servidor PUI. Emite un JWT de sesión.
Rate limit: 10 solicitudes / minuto / IP (protección anti-fuerza bruta) Autenticación: Ninguna (endpoint público)
Headers requeridos:
| Header | Valor | Descripción |
|---|---|---|
X-Tenant-Id | RFC de la institución | Identifica qué institución está siendo consultada |
Content-Type | application/json |
Body:
{
"usuario": "PUI",
"clave": "contraseña_secreta_de_la_institución"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
usuario | string | Sí | Debe ser exactamente "PUI" (3 caracteres). Cualquier otro valor devuelve 401 |
clave | string | Sí | Contraseña configurada por la institución en ConectaPUI |
Respuesta exitosa 200:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Errores:
| Código | Causa |
|---|---|
401 | usuario ≠ "PUI", clave incorrecta, o institución no configurada |
429 | Más de 10 intentos por minuto desde la misma IP |
Nota de seguridad: Los mensajes de error son idénticos en todos los casos de 401 (
"Credenciales inválidas") para no revelar si eltenantId, elusuarioo laclaveson el campo incorrecto. Todos los intentos (exitosos y fallidos) se registran en la bitácora de auditoría con la IP de origen.
POST /pui/activar-reporteLa PUI notifica el ingreso de un nuevo reporte de persona desaparecida. Desencadena las fases 1 y 2 de búsqueda.
Autenticación: Bearer token obtenido en /pui/login
Header requerido: X-Tenant-Id: <RFC>
Body:
{
"id": "RPT-2024-00123",
"curp": "XAXX010101HMCLRS00",
"fechaDesaparicion": "2023-11-15",
"nombre": "JUAN",
"primerApellido": "PÉREZ",
"segundoApellido": "GARCÍA",
"sexo": "H",
"descripcion": "Texto libre opcional"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string | Sí | ID único del reporte asignado por la PUI |
curp | string | Sí | CURP de 18 caracteres. Validado con regex ^[A-Z0-9]{18}$ |
fechaDesaparicion | string ISO 8601 | No | Fecha de desaparición. Si se omite, se omite la fase 2 histórica |
nombre | string | No | Nombre(s) de la persona |
primerApellido | string | No | |
segundoApellido | string | No | |
sexo | string | No | |
descripcion | string | No | Texto libre, máximo 4096 chars almacenados |
Lo que ocurre internamente:
1. Se almacena el reporte en DynamoDB con status="active"
2. Se encola en SQS → Fase 1 Worker (búsqueda inmediata en registro más reciente)
3. Se encola en SQS → Fase 2 Worker (búsqueda histórica hasta 12 años)
4. La Fase 3 se activa automáticamente cada hora vía EventBridge
5. Se registra en bitácora de auditoría (Anexo 5)
Respuesta exitosa 200:
{
"message": "Reporte activado correctamente",
"id": "RPT-2024-00123",
"receivedAt": "2024-01-15T14:30:00.000Z"
}
POST /pui/activar-reporte-pruebaIdéntico a /pui/activar-reporte pero no dispara ninguna búsqueda real. Para validaciones en ambiente Sandbox.
El reporte se almacena con status="test" e isTest=true. La fase 3 no lo incluirá en búsquedas periódicas.
Respuesta exitosa 200:
{
"message": "Reporte de prueba recibido correctamente",
"id": "RPT-TEST-001",
"receivedAt": "2024-01-15T14:30:00.000Z"
}
POST /pui/desactivar-reporteLa PUI notifica que un reporte debe cerrarse (persona localizada, caso resuelto, etc.). Detiene la fase 3 de búsqueda continua.
Autenticación: Bearer token obtenido en /pui/login
Header requerido: X-Tenant-Id: <RFC>
Body:
{
"id": "RPT-2024-00123",
"motivo": "Persona localizada con vida"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string | Sí | ID del reporte a desactivar |
motivo | string | No | Razón del cierre |
Lo que ocurre internamente:
1. Se actualiza report.status = "inactive"
2. Se actualiza gsi1sk = "REPORT#INACTIVE"
→ El scheduler de Fase 3 ya no lo incluye (busca solo REPORT#ACTIVE)
3. Se registra en bitácora de auditoría
Respuesta exitosa 200:
{
"message": "Reporte desactivado correctamente",
"id": "RPT-2024-00123",
"desactivadoAt": "2024-01-15T18:00:00.000Z"
}
Estos endpoints son llamados por ConectaPUI hacia el servidor de la PUI, no por el cliente. Se documentan para referencia de la PUI y para auditoría.
POST <puiBaseUrl>/notificar-coincidenciaConectaPUI llama este endpoint en el servidor de la PUI cuando encuentra un registro coincidente.
Cuándo se llama:
Body enviado:
{
"id_reporte": "RPT-2024-00123",
"curp": "XAXX010101HMCLRS00",
"fase_busqueda": "1",
"lugar_nacimiento": "CDMX",
"nombre": "JUAN",
"primer_apellido": "PÉREZ",
"segundo_apellido": "GARCÍA",
"fecha_nacimiento": "1990-05-15",
"sexo": "H",
"telefono": "+525512345678",
"correo": "juan@email.com",
"domicilio": {
"calle": "INSURGENTES SUR",
"numero": "123",
"colonia": "DEL VALLE",
"cp": "03100",
"municipio": "BENITO JUÁREZ",
"entidad": "CIUDAD DE MÉXICO"
}
}
Regla crítica — Fase 1: Los campos
tipo_evento,fecha_evento,descripcion_lugar_eventoydireccion_eventono se incluyen en fase 1 (Manual Técnico PUI v1.0, sección de Fase 1).
Fase 2 y 3: Se agregan los campos de evento:
{
"...campos_anteriores": "...",
"tipo_evento": "HOSPITALIZACION",
"fecha_evento": "2023-10-01",
"descripcion_lugar_evento": "Hospital General",
"direccion_evento": "Av. Insurgentes Norte 1200"
}
| Campo | Fase 1 | Fase 2/3 | Descripción |
|---|---|---|---|
lugar_nacimiento | ✅ Siempre | ✅ Siempre | Derivado automáticamente de los chars 11-12 del CURP |
tipo_evento | ❌ Omitido | ✅ Incluido | Tipo de interacción institucional |
fecha_evento | ❌ Omitido | ✅ Incluido | Fecha del evento |
descripcion_lugar_evento | ❌ Omitido | ✅ Incluido | |
direccion_evento | ❌ Omitido | ✅ Incluido |
Reintentos: 3 intentos con backoff exponencial en errores 5xx. En error 401, se renueva el token y se reintenta una vez.
POST <puiBaseUrl>/busqueda-finalizadaConectaPUI notifica a la PUI que la búsqueda histórica (Fase 2) ha concluido.
Cuándo se llama: Siempre al terminar la Fase 2, independientemente de si se encontraron coincidencias. Si el reporte no tenía fechaDesaparicion, se llama inmediatamente.
Body:
{
"id": "RPT-2024-00123",
"institucion_id": "RFC_DE_LA_INSTITUCION"
}
Todos estos endpoints requieren
Authorization: Bearer <cognito-token>. EltenantIdse extrae automáticamente del JWT — no se pasa por URL ni body.
POST /auth/registerRegistra una nueva institución.
Body:
{
"rfc": "XAXX010101000",
"institutionName": "Mi Institución AC",
"email": "admin@institucion.gob.mx",
"password": "SecurePass123!",
"phone": "+525512345678",
"referralCode": "ABC123"
}
POST /auth/loginInicia sesión de un usuario institucional.
{
"email": "admin@institucion.gob.mx",
"password": "SecurePass123!"
}
Respuesta: { "token": "...", "user": { ... } }
GET /tenants/:tenantIdObtiene la configuración de la institución. Los campos encryptedClave y encryptedToken nunca se devuelven.
Respuesta:
{
"tenantId": "XAXX010101000",
"rfc": "XAXX010101000",
"institutionName": "Mi Institución AC",
"email": "admin@institucion.gob.mx",
"puiConfigured": true,
"puiBaseUrl": "https://pui.institucion.gob.mx/api",
"createdAt": "2024-01-01T00:00:00.000Z"
}
PATCH /tenants/:tenantId/pui-configActualiza las credenciales de integración PUI.
{
"puiBaseUrl": "https://pui.institucion.gob.mx/api",
"puiClave": "contraseña_minimo_8_chars"
}
La puiClave se cifra con AES-256-GCM antes de almacenarse. Nunca se persiste en texto claro ni se devuelve en ningún endpoint.
GET /records?limit=20Lista los registros más recientes del padrón de la institución.
Respuesta:
{
"items": [
{
"curp": "XAXX010101HMCLRS00",
"nombre": "JUAN",
"primerApellido": "PÉREZ",
"fechaNacimiento": "1990-05-15",
"sexo": "H",
"lugarNacimiento": "CDMX",
"updatedAt": "2024-01-15T10:00:00.000Z"
}
]
}
GET /records/:curpObtiene el registro más reciente de un CURP específico. Devuelve 404 si no existe.
POST /records/searchBúsqueda flexible.
{
"curp": "XAXX010101HMCLRS00"
}
{
"fromDate": "2023-01-01",
"toDate": "2023-12-31"
}
POST /uploads/initiateSolicita una URL pre-firmada de S3 para subir un archivo Excel.
{ "fileName": "registros_enero_2024.xlsx" }
Respuesta:
{
"uploadUrl": "https://s3.amazonaws.com/...",
"s3Key": "uploads/TENANT/2024-01-15/uuid.xlsx",
"expiresIn": 300
}
POST /uploads/processInicia el procesamiento del archivo ya subido a S3.
{ "s3Key": "uploads/TENANT/2024-01-15/uuid.xlsx" }
El procesamiento es asíncrono — se encola en SQS y el worker lo procesa. El estado se puede consultar en el dashboard.
GET /search/flow?status=active&limit=50Lista todos los reportes con su estado de flujo actual.
| Query param | Valores | Default |
|---|---|---|
status | active, inactive, test | active |
limit | 1-100 | 20 |
Respuesta:
{
"items": [
{
"reportId": "RPT-2024-00123",
"curp": "XAXX010101HMCLRS00",
"nombre": "JUAN",
"status": "active",
"totalCoincidencias": 3,
"fase1Coincidencias": 1,
"fase2Coincidencias": 2,
"fase3Coincidencias": 0,
"steps": [
{ "id": "received", "status": "completed", "label": "Reporte recibido de PUI" },
{ "id": "phase1_search", "status": "completed", "label": "Fase 1 — Búsqueda de datos básicos" },
{ "id": "phase1_notify", "status": "completed", "label": "Notificación fase 1 enviada a PUI" },
{ "id": "phase2_search", "status": "completed", "label": "Fase 2 — Búsqueda histórica" },
{ "id": "phase2_notify", "status": "completed", "label": "Coincidencias fase 2 notificadas" },
{ "id": "busqueda_finalizada","status": "completed","label": "Búsqueda histórica finalizada" },
{ "id": "phase3_continuous", "status": "running", "label": "Fase 3 — Búsqueda continua activa" }
]
}
],
"total": 1
}
Los valores posibles de status por paso son: pending, running, completed, skipped, failed.
GET /search/flow/:reportIdDetalle de flujo de un reporte específico. Misma estructura que el ítem anterior.
GET /dashboard/stats{
"totalRecords": 15420,
"totalUploads": 12,
"activeReports": 7,
"totalCoincidencias": 23,
"recentActivity": [...]
}
GET /dashboard/pui-activity?limit=100Historial de eventos PUI (entrantes y salientes). Cada ítem:
{
"id": "uuid",
"type": "notificar-coincidencia",
"reportId": "RPT-2024-00123",
"direction": "outbound",
"timestamp": "2024-01-15T14:35:00.000Z",
"description": "Coincidencia notificada al PUI"
}
Tipos de evento: login-exitoso, login-failed, activar-reporte, activar-reporte-prueba, desactivar-reporte, notificar-coincidencia, busqueda-finalizada.
GET /dashboard/api-logs?limit=100Log de todas las llamadas HTTP. Cada ítem:
{
"requestId": "uuid",
"method": "POST",
"url": "/api/v1/pui/activar-reporte",
"statusCode": 200,
"durationMs": 145,
"timestamp": "2024-01-15T14:30:00.000Z",
"direction": "inbound",
"requestBody": { "...": "sanitizado" },
"error": null
}
Los campos
password,clave,token,secret,puiClave,puiToken,encryptedClaveyencryptedTokennunca aparecen en los logs — son reemplazados por[REDACTED]antes de escribir.
Todos los errores siguen el formato:
{
"statusCode": 400,
"message": "CURP must match ^[A-Z0-9]{18}$",
"error": "Bad Request"
}
| Código | Significado | Causa más común |
|---|---|---|
400 | Bad Request | Campo requerido faltante o formato inválido (CURP, fecha, email) |
401 | Unauthorized | Token ausente, expirado o inválido |
403 | Forbidden | El tenant del token no coincide con el recurso solicitado |
404 | Not Found | Registro o reporte no encontrado |
409 | Conflict | Tenant ya registrado con ese RFC |
429 | Too Many Requests | Rate limit excedido (100/min global, 10/min en /pui/login) |
500 | Internal Server Error | Error de infraestructura — revisar logs en CloudWatch |
| Control | Implementación | Ubicación |
|---|---|---|
| Validación de entrada | class-validator en todos los DTOs. CURP valida contra ^[A-Z0-9]{18}$. Fechas con @IsDateString(). Strings con @MaxLength(). | src/modules/*/dto/*.ts |
| Inyección | DynamoDB no usa SQL. Las queries usan ExpressionAttributeValues parametrizadas — no hay interpolación de strings en queries. | src/common/dynamodb/dynamodb.service.ts |
| Secretos en código | Cero secretos en código. ENCRYPTION_KEY y PUI_JWT_SECRET se resuelven en tiempo de ejecución desde AWS Secrets Manager. | src/common/utils/secrets.util.ts |
| JWT seguro | Algoritmo HS256 explícito. Verificación rechaza alg: none. TTL de 1 hora. Subject siempre validado (sub === "PUI"). | src/common/guards/pui-jwt.guard.ts |
| Cifrado de credenciales | puiClave y puiToken almacenados con AES-256-GCM (IV aleatorio por operación). Nunca en texto claro. | src/common/utils/crypto.util.ts |
| Error messages opacos | Todos los 401 de /pui/login devuelven el mismo mensaje genérico, independientemente de cuál campo falló. | src/modules/pui/pui.controller.ts |
| Dependencias | @aws-sdk/* v3, axios v1.6+, class-validator v0.14+, jsonwebtoken v9. npm audit integrado en CI/CD. | apps/backend/package.json |
| Control | Implementación | Ubicación |
|---|---|---|
| HTTPS obligatorio | HSTS con max-age=31536000; includeSubDomains; preload en todos los responses | CDK: conecta-pui-stack.ts |
| CORS restrictivo | Producción: solo conectapui.com, www.conectapui.com, app.conectapui.com. Dev: solo localhost:3000. No AllOrigins. | CDK: allowedOrigins por stage |
| Security headers | X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block, Referrer-Policy: strict-origin-when-cross-origin, Cache-Control: no-store | CDK: Gateway Responses |
| Rate limiting | Global: 100 req/min/IP. /pui/login: 10 req/min/IP (brute-force). Aplicado via ThrottlerGuard global en NestJS. | src/app.module.ts, src/modules/pui/pui.controller.ts |
| API Gateway throttling | 1,000 req/s rate, 2,000 burst. Primer nivel de defensa antes de llegar a Lambda. | CDK: deployOptions.throttlingRateLimit |
| No log de datos sensibles | API GW dataTraceEnabled: false. Interceptor sanitiza campos sensibles antes de escribir a DynamoDB. | CDK + api-logger.interceptor.ts |
# Verificar vulnerabilidades en todas las dependencias
npm audit --workspaces
# Actualizar parches disponibles
npm audit fix --workspaces
Dependencias de riesgo identificadas:
| Paquete | Versión | Estado | Acción |
|---|---|---|---|
xlsx | ^0.18.5 | CVEs de prototype pollution conocidas | Migrar a exceljs en siguiente sprint |
aws-serverless-express | ^3.4.0 | Deprecated | Reemplazado por @vendia/serverless-express (ya en deps) — remover el deprecated |
| Resto del stack | — | Sin CVEs activos al 2026-03-19 | Mantener actualizados en CI/CD |
Política de CI/CD: El pipeline debe ejecutar npm audit --audit-level=high y fallar si hay vulnerabilidades de severidad Alta o Crítica.
ConectaPUI implementa el Anexo 5 del Manual Técnico PUI mediante dos tablas de registro independientes:
MAIN_TABLE, prefijo PUI_EVENT#)Registra todas las interacciones con la PUI, tanto entrantes como salientes.
Estructura de cada registro:
{
"pk": "PUI_EVENT#<uuid>",
"sk": "REPORT#<reportId>",
"gsi1pk": "TENANT#<tenantId>",
"gsi1sk": "PUI_EVENT#<ISO-timestamp>",
"eventId": "uuid-v4",
"tenantId": "RFC_DE_LA_INSTITUCION",
"reportId": "RPT-2024-00123",
"eventType": "notificar-coincidencia",
"direction": "outbound",
"payload": "{\"fase\":\"1\",\"curp\":\"XAXX...\"}",
"createdAt": "2024-01-15T14:35:00.000Z",
"ttl": 1234567890
}
Eventos registrados:
| Evento | Dirección | Cuándo |
|---|---|---|
login-exitoso | inbound | PUI autenticó correctamente |
login-failed | inbound | Intento fallido (incluye IP y razón: usuario_invalido, clave_invalida, tenant_no_encontrado) |
activar-reporte | inbound | PUI envió un reporte real |
activar-reporte-prueba | inbound | PUI envió un reporte de prueba |
desactivar-reporte | inbound | PUI cerró un reporte |
notificar-coincidencia | outbound | ConectaPUI notificó una coincidencia a la PUI |
busqueda-finalizada | outbound | ConectaPUI notificó fin de búsqueda histórica |
API_LOGS_TABLE)Registra toda llamada HTTP tanto entrante (PUI → nosotros) como saliente (nosotros → PUI), incluyendo:
| Característica | Valor |
|---|---|
| TTL de registros | 90 días (campo ttl en DynamoDB con expiración automática) |
| Inmutabilidad | DynamoDB no tiene operación de DELETE masivo. Los registros de auditoría usan Put (append-only en la práctica) |
| Acceso | Solo la Lambda con el rol IAM de la aplicación puede escribir/leer. No hay acceso público |
| Cifrado en reposo | DynamoDB cifrado con AWS KMS (habilitado por defecto en todas las tablas) |
| Cifrado en tránsito | HTTPS/TLS 1.2+ obligatorio en todos los endpoints |
Vía API (dashboard):
GET /dashboard/pui-activity?limit=100
GET /dashboard/api-logs?limit=100
Vía AWS CLI (auditoría directa):
aws dynamodb query \
--table-name conectapui-prod-main \
--index-name GSI1 \
--key-condition-expression "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)" \
--expression-attribute-values '{
":pk": {"S": "TENANT#<RFC>"},
":prefix": {"S": "PUI_EVENT#"}
}' \
--scan-index-forward false \
--region us-west-2
| Variable | Descripción | Fuente |
|---|---|---|
MAIN_TABLE | Tabla DynamoDB principal | CDK output |
UPLOADS_TABLE | Tabla de cargas | CDK output |
API_LOGS_TABLE | Tabla de logs API | CDK output |
UPLOADS_BUCKET | Bucket S3 para Excel | CDK output |
USER_POOL_ID | Cognito User Pool | CDK output |
USER_POOL_CLIENT_ID | Cognito App Client | CDK output |
PHASE1_QUEUE_URL | SQS Fase 1 | CDK output |
PHASE2_QUEUE_URL | SQS Fase 2 | CDK output |
PHASE3_QUEUE_URL | SQS Fase 3 | CDK output |
PUI_NOTIFICATION_QUEUE_URL | SQS notificador PUI | CDK output |
ENCRYPTION_KEY_SECRET_ARN | ARN secret AES-256 | CDK output |
PUI_JWT_SECRET_ARN | ARN secret JWT PUI | CDK output |
Para desarrollo local, usar
ENCRYPTION_KEYyPUI_JWT_SECRETdirectamente como variables de entorno (sin Secrets Manager). Verapps/backend/.env.example.