feat(api-externa): expone indicadores del Widget Engine vía API M2M
El Portal AUP necesita alimentar sus KPIs con indicadores del portal, pero
por política de seguridad no puede conectarse a la base del data lake. El JWT
actual no sirve para eso: está atado a una fila de users y expira a las 8h,
así que un consumidor tendría que guardar la contraseña de un empleado y
re-loguearse cada 8 horas.
Se agrega una fachada estable sobre el Widget Engine, con credencial de larga vida, revocable y de alcance acotado.
Endpoints M2M (X-API-Key): GET /api/external/v1/metrics catálogo de lo expuesto GET /api/external/v1/metrics/{key} un escalar (1 fila, 1 número) GET /api/external/v1/widgets/{key} filas completas (series, tablas)
Administración (JWT admin): emitir/revocar keys, ver consumo, definir modo de exposición por widget y verificar un widget antes de exponerlo.
Tres compuertas independientes, todas deben pasar:
- key válida, activa, no revocada, no expirada, con scope metrics:read
- categoría del widget dentro del alcance del key
- widget_definitions.external_mode habilita el endpoint pedido
external_mode es de tres valores ('none'|'scalar'|'full') y no un booleano
porque exponer el agregado y exponer el desglose son decisiones distintas:
fin.ebitda_total devuelve un consolidado, fin.top_empresas_ebitda devuelve el
EBITDA empresa por empresa. Con un booleano, voltearlo pasaría en silencio de
lo primero a lo segundo. Un widget en 'scalar' da 404 en /widgets.
Notas de seguridad:
- El key crudo se devuelve una sola vez; la BD guarda solo su SHA-256.
- 401 idéntico para key desconocida, revocada, inactiva y expirada, para que no se pueda distinguir una de otra. El motivo real va al log de la app.
- 404 idéntico para inexistente, 'none' y "expuesto en otro modo", para no permitir enumerar el catálogo interno.
- Los fallos de auth no escriben en la bitácora de BD: sería un amplificador de escrituras ante un barrido de keys.
- Params en modo estricto: un param no declarado es 400, no se ignora.
- Tope de filas configurable con bandera
truncatedexplícita.
Default cerrado: la migración no expone ningún widget.
Verificación: 44 pruebas nuevas (144 en total en la suite unitaria), grafo de modelos OK, migración aplicada dos veces contra Postgres 15 para comprobar idempotencia y que los widgets preexistentes quedan en modo 'none'.
Pendientes conocidos: rate limiting y pantallas de admin en React.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com