Skip to content

farox-coop/missing-captcha

Repository files navigation

Missing Captcha (MVP)

Monorepo con:

  • apps/api: Fastify + TypeScript + Postgres
  • packages/widget: widget embebible (dist/widget.js)
  • packages/shared: tipos y helpers crypto

Requisitos

  • Docker + Docker Compose

Quickstart (demo completa)

cp .env.example .env
make demo

Esto:

  • levanta postgres + migrator + api
  • refresca casos reales desde Missing Children en la DB local
  • crea un siteKey para localhost
  • compila widget.js y widget.css
  • genera demo/index.html
  • sirve la demo en http://localhost:3000

make demo ejecuta ingest para refrescar los casos reales antes de servir la demo y la publica desde un contenedor temporal nginx:alpine.

Setup

cp .env.example .env
make setup DOMAINS="localhost,tu-dominio.gob"

Esto ya corre todos los pasos necesarios para que funcione, incluyendo el ingest.

Notas del docker compose por default:

  • Postgres queda solo dentro de la red interna de Docker (sin puerto publicado al host).
  • El API se publica solo en loopback del host (127.0.0.1) usando:
    • MISSING_CAPTCHA_API_HOST_BIND
    • MISSING_CAPTCHA_API_HOST_PORT
  • Dentro del contenedor, el API escucha en 8000 por default; para Docker no hace falta declarar PORT en .env.
  • Esto facilita correr nginx delante del API sin exponer el puerto raw públicamente.

Ingest de casos

make ingest
  • Test live contra el sitio externo:
make test-integration
  • El ingest consume datos públicos desde:
    • https://www.missingchildren.org.ar/pages/galeria_ajax.php?limite=<N>&offset=<N>&situacionBusqueda=perdidos
    • https://www.missingchildren.org.ar/pages/detalles.php?id=<id>
  • El ingest usa un parser HTML real (cheerio) para leer la galería y las fichas de detalle.
  • El sync es incremental por source_external_id: inserta nuevos casos, actualiza existentes, elimina casos marcados como encontrados y también elimina casos ausentes cuando su updated_at supera el umbral configurado.
  • contact_info se llena con líneas fallback normalizadas (por ejemplo teléfono + email) y el CTA de WhatsApp se guarda en report_url.
  • Durante la ejecución muestra progreso por página y aplica timeout por request para evitar bloqueos largos por red.
  • Timeout configurable con INGEST_REQUEST_TIMEOUT_MS (default: 12000 ms).
  • El API ya incluye un scheduler interno diario. Al arrancar, corre ingest solo si la DB todavía no tiene casos. Variables:
    • INGEST_SCHEDULER_ENABLED (default: true)
    • INGEST_SCHEDULER_INTERVAL_MS (default: 86400000)
    • INGEST_DELETE_MISSING_AFTER_DAYS (default: 30)

Tests

make test
make test-integration
make test-all
  • make test corre la suite normal.
  • make test-integration corre un test real contra Missing Children (galería + detalle).
  • make test-all corre lint, tests normales y el test live dentro de docker.
  • todos los targets de make corren dentro de Docker.

Embed snippet

<link rel="stylesheet" href="/ruta-a-tu-cdn/widget.css" />
<div
  data-missing-captcha
  data-sitekey="PUBLIC_SITE_KEY"
  data-api-base="http://localhost:8000"
  data-theme="light"
  data-locale="es"
  data-layout="split"
></div>
<script src="/ruta-a-tu-cdn/widget.js" defer></script>

Customización rápida del widget

  • data-layout="split" (default): foto + tarjeta lateral.
  • data-layout="stacked": apila el contenido en una sola columna.
  • data-layout="compact": reduce gaps y anchos para espacios más chicos.

También podés ajustar estilos desde el contenedor usando CSS variables:

<div
  data-missing-captcha
  data-sitekey="PUBLIC_SITE_KEY"
  data-api-base="http://localhost:8000"
  data-layout="split"
  style="--rc-stage-width: 420px; --rc-side-width: 340px; --rc-side-title-size-mobile: 25px; --rc-cta-bg: #0b5cab;"
></div>

Variables útiles:

  • --rc-stage-width
  • --rc-photo-max-width
  • --rc-side-width
  • --rc-side-min-width
  • --rc-layout-gap
  • --rc-wrap-padding
  • --rc-side-title-size
  • --rc-side-title-size-mobile
  • --rc-marker-size
  • --rc-marker-font-size
  • --rc-cta-bg
  • --rc-cta-bg-hover

Eventos opcionales para el host

Además de missing-captcha-success y missing-captcha-error, el widget ahora emite missing-captcha-action cuando el usuario hace click en:

  • Brindar informaciónaction: "report_info"
  • Más infoaction: "more_info"

El detail del evento incluye:

  • action
  • href
  • caseId
  • caseName
  • verified

También se puede capturar mediante callback global opcional:

  • window.MissingCaptcha.onAction = (detail) => { ... }

Esto permite que el sitio host traduzca esos clicks a eventos propios (por ejemplo Google Analytics) sin acoplar el widget a una implementación específica de analytics.

Endpoints

  • POST /v1/challenges
  • POST /v1/challenges/:id/verify
  • POST /v1/verified-tokens/verify
  • POST /v1/events
  • GET /healthz
  • GET /readyz

Verificación server-to-server del verifiedToken

El host remoto puede validar el token resuelto por el widget con:

POST /v1/verified-tokens/verify
{
  "verifiedToken": "...",
  "siteKey": "rc_pk_..."
}
  • siteKey es opcional pero recomendado para asegurar que el token corresponde al sitio esperado.
  • En despliegues same-host, conviene que este endpoint quede accesible solo desde localhost vía nginx. Hay un ejemplo listo en deploy/nginx/missing-captcha.same-host.conf.example.
  • Respuesta exitosa:
{
  "ok": true,
  "token": {
    "challengeId": "...",
    "siteKeyId": "...",
    "type": "missing_verified",
    "expiresAt": "2026-04-09T12:34:56.000Z"
  }
}
  • Errores esperables:
    • INVALID_VERIFIED_TOKEN
    • VERIFIED_TOKEN_EXPIRED
    • INVALID_SITE_KEY
    • VERIFIED_TOKEN_SITE_MISMATCH

Seguridad MVP implementada

  • Verificación 100% server-side.
  • Integridad del payload con firma HMAC.
  • TTL de challenge + single-use (used_at).
  • Lockout por demasiados intentos.
  • JWT de verificación de corta vida.
  • Allowlist de dominios por siteKey.
  • Sin almacenamiento de IP en events.

Accesibilidad y CSP

  • Modo visual con botones navegables por teclado (Tab, Enter, flechas).
  • Estados con aria-live y foco visible.
  • CSP compatible: el widget ya no inyecta estilos inline; usa widget.css externo.

Notas

  • Provider actual es demo (apps/api/src/jobs/ingest_missing_children.ts) con datos públicos/no sensibles.
  • La selección de casos en runtime ya no rota por categorías; el origen activo se simplificó a perdidos.
  • Para producción: reemplazar ingest demo por feed/API acordado y agregar A11y avanzada + métricas.
  • Para integraciones productivas, una opción común es servir widget.js y widget.css desde el mismo sitio host (o desde un CDN propio) y dejar el API detrás de nginx, publicando hacia afuera solo las rutas necesarias.

Troubleshooting rápido

  • Si make up falla por imágenes viejas:
docker compose down -v
docker compose build --no-cache
make up

About

Captcha que muestra casos de personas desaparecidas desde missingchildren.org.ar

Topics

Resources

Stars

Watchers

Forks

Contributors