Monorepo con:
apps/api: Fastify + TypeScript + Postgrespackages/widget: widget embebible (dist/widget.js)packages/shared: tipos y helpers crypto
- Docker + Docker Compose
cp .env.example .env
make demoEsto:
- levanta
postgres + migrator + api - refresca casos reales desde Missing Children en la DB local
- crea un
siteKeyparalocalhost - compila
widget.jsywidget.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.
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_BINDMISSING_CAPTCHA_API_HOST_PORT
- Dentro del contenedor, el API escucha en
8000por default; para Docker no hace falta declararPORTen.env. - Esto facilita correr
nginxdelante del API sin exponer el puerto raw públicamente.
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=perdidoshttps://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 suupdated_atsupera el umbral configurado. contact_infose llena con líneas fallback normalizadas (por ejemplo teléfono + email) y el CTA de WhatsApp se guarda enreport_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:12000ms). - 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)
make test
make test-integration
make test-allmake testcorre la suite normal.make test-integrationcorre un test real contra Missing Children (galería + detalle).make test-allcorre lint, tests normales y el test live dentro de docker.- todos los targets de
makecorren dentro de Docker.
<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>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
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ón→action: "report_info"Más info→action: "more_info"
El detail del evento incluye:
actionhrefcaseIdcaseNameverified
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.
POST /v1/challengesPOST /v1/challenges/:id/verifyPOST /v1/verified-tokens/verifyPOST /v1/eventsGET /healthzGET /readyz
El host remoto puede validar el token resuelto por el widget con:
POST /v1/verified-tokens/verify
{
"verifiedToken": "...",
"siteKey": "rc_pk_..."
}siteKeyes 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_TOKENVERIFIED_TOKEN_EXPIREDINVALID_SITE_KEYVERIFIED_TOKEN_SITE_MISMATCH
- 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.
- Modo visual con botones navegables por teclado (
Tab,Enter, flechas). - Estados con
aria-livey foco visible. - CSP compatible: el widget ya no inyecta estilos inline; usa
widget.cssexterno.
- 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.jsywidget.cssdesde el mismo sitio host (o desde un CDN propio) y dejar el API detrás denginx, publicando hacia afuera solo las rutas necesarias.
- Si
make upfalla por imágenes viejas:
docker compose down -v
docker compose build --no-cache
make up