Skip to content

feat(platform): 0.8.0 — errores 500 opacos, auth opt-in con serviceId, logger endurecido, niveles, client-errors, higiene - #21

Merged
donchelo merged 15 commits into
mainfrom
feat/platform-0.8.0
Oct 30, 2026
Merged

donchelo merged 15 commits into
mainfrom
feat/platform-0.8.0

Conversation

@donchelo

@donchelo donchelo commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Alcance

@ai4u/platform 0.8.0 (seguridad + robustez, Ola 2 del plan). Afecta a todos los consumidores de platform y a todos los tenants; no hardcodea ningún tenant. Solo código del kernel: no se publica, no se crea tag, no se toca ningún otro repo ni secretos. Un PR, un commit por grupo (más docs y un ajuste de test de mc-sso).

Hallazgo -> commit -> test

Grupo Hallazgos auditoría Commit Tests
P1 errores #1 4b0f133 tests/errors-500-opaco.test.ts
P2 auth #2, #3 ebfc292 tests/auth-scope.test.ts
P3 logger + request id #4, #6, #7, #8, #10 14dea1b tests/logger-hardening.test.ts
P4 niveles #5 8c49885 tests/http-log-levels.test.ts
P5 client-errors #9 aa3574a tests/client-errors-hardening.test.ts
P6 higiene #12 (parcial), #11 (NO, ver abajo) d084926 tests/platform-version.test.ts
docs - 6e01ac7 -
A1 estado de identidad en globalThis (revisión) - 7501b73 tests/auth-multicopy.test.ts
A2/M2 client-errors neutraliza path/service - 635e5c7 tests/client-errors-hardening.test.ts
M1/B5 sin details en 5xx no operacional - ee7cf9d tests/errors-500-opaco.test.ts
M5 4xx no operacional sigue en error - 6d96b9a tests/http-log-levels.test.ts
M3/M4/B1/M6 logger (buffers, sanitize seguro, tokens, breaker) - ed8e51d, f1d6652 (B1 fail-closed) tests/logger-hardening2.test.ts
docs revisión (B4, M6, 401/403) - último commit docs -
ajuste test mc-sso - 2837c87 packages/mc-sso/tests/handler.test.ts (solo el test de interop)

Qué cambia de contrato visible (detalle en CHANGELOG 0.8.0)

  • Un 500 no operacional responde {error:"Error interno", code, requestId} en vez de err.message. 4xx y AppError operacionales conservan mensaje. Consumidores que muestren error de un 500 al usuario deben revisarse (auditoría previa pendiente del plan).
  • Un 4xx lanzado y operacional se loguea warn (antes error): los 401/403 lanzados dejan de verse en ERROR. AppError con isOperational:false sigue en error.
  • x-request-id entrante inválido se regenera; service/path raros de client-errors se neutralizan (no se pierde el reporte); el rate limit de client-errors usa x-vercel-forwarded-for/x-real-ip primero.
  • Identity expone serviceId. engines.node >=22. Cada línea de log lleva platformVersion.
  • El endurecimiento de auth es opt-in (readIdentity(req, {serviceId, allowedTenants, acceptLegacy}), configureIdentity). Sin opciones el comportamiento es el de 0.7.0; no se deriva nada de SERVICE_ID. Un WARN por proceso cuando se lee una sesión sin serviceId configurado.

Qué NO se hizo

  • @vercel/oidc sigue en ^3.8.9: la 4.0.5 mantiene la API (tsc y tests pasan) pero arrastra @vercel/cli-config con zod fijado a 4.1.11, así que npm audit --omit=dev sigue en 3 high. No cierra el hallazgo.
  • Sin peerDependencies de next/react: el código no los importa (grep).
  • devDependencies sin tocar: npm install -D vitest@5 da ERESOLVE en el monorepo.
  • isModuleAllowed y requireModule por defecto siguen con null/[] = todo (decisión pendiente de Mariano, documentada). Solo se agregó el modo estricto opt-in.
  • Hallazgos docs(platform,contracts): README con kernel como fuente y estado real de 0.7.0 #13-ci: concurrency y timeout-minutes en ci.yml #18 (docs obsoletas, SSH en lock, SENSITIVE_KEY, etc.) fuera de alcance.
  • Tags, publicación y espejo: pendiente (PASO HUMANO scripts/release.sh platform 0.8.0 tras merge).

Evidencia

Rojo (antes del fix) -> verde, por grupo (salida real):

  • P1: Tests 2 failed | 4 passed (6) (cuerpo del 500 contenía connect ECONNREFUSED 10.0.0.5:5432 ...; AppError 502 no operacional devolvía su mensaje) -> verde.
  • P2: Tests 9 failed | 1 passed (10) (configureIdentity is not a function, cookie de otra app/tenant aceptada, sin WARN, isModuleAllowedStrict inexistente) -> verde.
  • P3 logger: Tests 8 failed | 1 passed (9) (Bearer/token/correo en claro, TypeError: Converting circular structure, tenant pisado, fetch sin señal, ingest colgado = timeout de 10 s del test, flush concurrente no esperaba, lote perdido, sin retención) -> verde.
  • P3 request id: 2 failed | 1 passed (5000 caracteres reflejados) -> verde.
  • P4: Tests 5 failed | 3 passed (8) (4xx lanzado no llegaba a warn) -> verde.
  • P5: Tests 10 failed | 2 passed (12) (51 de 51 chunks leídos antes de cortar; XFF falsificable; Map sin tope; service/path libres) -> verde.
  • P6: el test fallaba por ../src/version inexistente -> verde.

Revisión independiente (segunda ronda): rojo -> verde por grupo: A1 2 fallidos de 2; A2/M2 8 fallidos de 18; M1/B5 2 fallidos de 9; M5 1 fallido de 9; M3/M4/B1/M6 5 fallidos de 6 (más 1 ajuste: el spread de extras también va en try/catch).

Final, igual que el CI, en el worktree: npm ci && npm run type-check && npm test && npm run build -> type-check sin errores TS; tests: platform 214 (eran 161; +53 nuevos), config 53, contracts 57, mc-sso 51, total 395 verdes, 0 fallos; git diff --quiet -- 'packages/*/dist' limpio (dist regenerado con npm run build y commiteado en cada grupo).

Verificación NO realizada: ningún consumidor real (no se probó contra una app desplegada); la compatibilidad de las 30 apps con el cuerpo "Error interno" no está auditada aquí.

🤖 Generated with Claude Code

https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG

donchelo and others added 15 commits October 30, 2026 12:15
…l cliente

toErrorResponse responde {error:"Error interno", code, requestId} cuando el error no es
operacional y el status es >= 500; el mensaje real solo va al log (redactado). Los 4xx y los
AppError operacionales conservan su mensaje.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…gacy opt-in y modo estricto de módulos

readIdentity delega en readMcSession de mc-sso 1.3.0 y acepta {serviceId, allowedTenants,
normalizeTenant, acceptLegacy}; configureIdentity fija el default por app. Identity expone
serviceId. Sin serviceId configurado emite un WARN por proceso. No se deriva nada de SERVICE_ID
ni cambia el comportamiento por defecto. Se agregan isModuleAllowedStrict, requireModule con
{strict:true} y la opción strictModules de withApiHandler (null/[] = ningún módulo);
isModuleAllowed conserva su semántica.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
… reservadas, timeout y flush con reintento) y x-request-id validado

- redactSecrets sobre msg y extras (recursivo, profundidad 4, tolera ciclos).
- tenant/userId/requestId del contexto no se pisan desde los extras.
- fetch al ingest con AbortSignal.timeout(2000).
- flush con promesa en vuelo compartida; el lote fallido se reencola con tope de 1000 y los
  envíos automáticos respetan un enfriamiento de 5 s tras un fallo.
- x-request-id entrante validado con /^[A-Za-z0-9._-]{1,128}$/ o se regenera.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…xx retornado

withApiHandler registra warn (sin stack) para AppError con status < 500 lanzados; los 5xx siguen
en error. Reduce el ruido de level=ERROR por simples 401/403/404.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
… limit por IP confiable y validación de service/path

- el cuerpo se lee como stream y se corta al pasar maxBytes (413) sin content-length.
- clave del rate limit: x-vercel-forwarded-for / x-real-ip antes que x-forwarded-for.
- Map del rate limit con tope de 5000 entradas y poda de las más viejas.
- service (/^[A-Za-z0-9._-]{1,80}$/) y path (pathname codificado que empieza con /) se validan o 400.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…a línea de log y version 0.8.0

- engines.node >=22 (el CI ya usa Node 22).
- cada registro de log lleva platformVersion (constante src/version.ts + test que la ata a
  package.json) para medir adopción en platform_logs; los extras no pueden pisarla.
- sin peerDependencies de next/react: el código no los importa (verificado con grep).
- @vercel/oidc se queda en ^3.8.9: la 4.0.5 mantiene la API pero sigue arrastrando
  @vercel/cli-config -> zod 4.1.11 exacto, así que no cierra los 3 high de npm audit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…ge-lock con la versión 0.8.0

Incluye 'Cambios visibles', 'Cómo adoptar el endurecimiento de auth', la decisión pendiente sobre
allowedModules vacío y lo que no se hizo (@vercel/oidc 4.x, peerDependencies, devDependencies).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…ntity (platform 0.8.0)

Identity ahora expone serviceId; se agrega la comprobación de que readIdentity con el serviceId de
otra app rechaza la cookie. Solo toca el test; no cambia mc-sso ni su dist.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…ico) vive en globalThis

Turbopack puede instanciar el módulo auth varias veces por proceso; con variables de módulo,
configureIdentity() en una copia no lo veía otra y el WARN salía una vez por copia. Se guarda en
globalThis[Symbol.for("@ai4u/platform/identity")].

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
… vez de rechazar el reporte

Un path con | [ ] ^ { } se registra tal cual; el resto de caracteres no seguros pasa a "_" y un path
sin "/" inicial se reemplaza por "/invalid". Un service fuera de /^[A-Za-z0-9._-]{1,80}$/ se
registra como "unknown-client". Se evita la regresión de 0.7.0 donde esos reportes daban 400.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…o operacionales 5xx

Además de ocultar el mensaje, se omite details cuando el error es no operacional y 5xx. Tests de
ExternalServiceError con details y de AppError 503 no operacional.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
Un AppError con isOperational:false (bug inesperado) sigue en error con stack aunque su status sea
4xx, como documenta errors/index.ts. Los 4xx operacionales lanzados siguen en warn.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…anitize que nunca lanza, métricas de tokens y circuit breaker

- Buffer/typed arrays se resumen ("[Buffer n bytes]"); objetos y arrays se truncan a 100 elementos.
- sanitize y el spread de extras van en try/catch (fallback {_sanitizeFailed:true}): un Proxy o getter
  hostil no rompe log.info.
- max_tokens/inputTokens/tokens_used y bypass dejan de enmascararse; access_token, apiToken, token,
  password, userPass siguen enmascarados.
- tras 3 fallos consecutivos del ingest, flushLogs() también espera 5 s de enfriamiento.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
…declarativo, duplicados del reencolado

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
Se enmascara toda clave con "pass" salvo una lista explícita de palabras seguras (por segmento) y
toda clave con "token" salvo un patrón explícito de conteo (maxTokens, inputTokens, tokens_used…).
Corrige huecos de la versión anterior (userpass, dbpass, PASS, secretTokens, my_tokens).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTgTv7S3gQwaoNFw5fZeG
@donchelo
donchelo marked this pull request as ready for review October 30, 2026 17:54
@donchelo
donchelo merged commit dbbaa1b into main Oct 30, 2026
1 check passed
@donchelo
donchelo deleted the feat/platform-0.8.0 branch October 30, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant