Files
cayce-ops/runtime/hardcap/test-admin-auth.mjs
T
cayce 88b2ad2980
ci / lint-test-leaks (push) Canceled after 0s
ci / lint-test-leaks (pull_request) Canceled after 0s
hardcap: auth middleware en /admin — token obligatorio, comparación en tiempo constante
- adminAuth: exige Authorization: Bearer <HARDCAP_ADMIN_TOKEN> (o
  X-Admin-Token). Comparación timingSafeEqual sobre sha256 de ambos
  valores (sin leak de longitud ni timing). Fail-closed: sin token
  configurado, TODAS las rutas admin responden 503.
- 401s ahora se loguean con método, ruta e IP (primera trazabilidad de
  acceso que tiene el admin API desde que existe).
- token generado (32 bytes hex) y almacenado SOLO en
  /srv/hardcap-backend/.env — nunca impreso ni versionado.
- test-admin-auth.mjs: 6 tests (sin token, incorrecto, vacío, correcto,
  header alternativo, misma-longitud-distinto) — todos pasan en la VPS.
- desplegado en src + dist; servicio reiniciado y activo.

Nota de contexto: el admin API carecía de auth desde su creación
(index.ts: \x27TODO: add auth middleware before production\x27) y estuvo
alcanzable sin credenciales en :3001 hasta la contención de hoy.
2026-10-31 17:39:38 +00:00

32 lines
1.9 KiB
JavaScript

// test-admin-auth — verifica el middleware de /admin (hardcap-backend).
// Ejecutar en la VPS: node /srv/hardcap-backend/runtime-tests/test-admin-auth.mjs
// (o node runtime/hardcap/test-admin-auth.mjs desde el repo)
// Requiere HARDCAP_ADMIN_TOKEN en el .env del servicio y el servicio activo en 127.0.0.1:3001.
import { readFileSync } from 'node:fs'
const BASE = 'http://127.0.0.1:3001'
const env = Object.fromEntries(
readFileSync('/srv/hardcap-backend/.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)])
)
const TOKEN = env.HARDCAP_ADMIN_TOKEN
if (!TOKEN) { console.error('HARDCAP_ADMIN_TOKEN no configurado — el test no puede pasar'); process.exit(2) }
let failed = 0
async function check(name, path, opts, expected) {
const r = await fetch(BASE + path, { ...opts, signal: AbortSignal.timeout(8000) })
const ok = r.status === expected
if (!ok) failed++
console.log(`${ok ? '✓' : '✗'} ${name}: esperado ${expected}, obtenido ${r.status}`)
}
await check('sin token → 401', '/admin/articles?status=QUEUED', {}, 401)
await check('token incorrecto → 401', '/admin/articles?status=QUEUED', { headers: { Authorization: 'Bearer ' + 'x'.repeat(64) } }, 401)
await check('token vacío en header → 401', '/admin/sources', { headers: { Authorization: 'Bearer ' } }, 401)
await check('token correcto → 200', '/admin/articles?status=QUEUED', { headers: { Authorization: `Bearer ${TOKEN}` } }, 200)
await check('header alternativo X-Admin-Token → 200', '/admin/sources', { headers: { 'x-admin-token': TOKEN } }, 200)
// la comparación debe ser en tiempo constante: un token de la misma longitud pero distinto NO pasa
await check('token misma longitud, distinto → 401', '/admin/articles?status=QUEUED', { headers: { Authorization: 'Bearer ' + 'y'.repeat(TOKEN.length) } }, 401)
console.log(failed ? `\n${failed} TESTS FALLARON` : '\nTODOS LOS TESTS PASARON')
process.exit(failed ? 1 : 0)