@heraldserver/server
v0.2.1
Published
Servidor de controle do Herald Protocol — gestão de Outposts, usuários e histórico de métricas, backed by Postgres. Sem dashboard/UI de dados (ver @herald/dashboard, congelado em ICA-34) — só a tela mínima de autorização do device flow (herald login).
Downloads
579
Maintainers
Readme
@heraldserver/server
Servidor de controle do Herald Protocol — gestão de Outposts, usuários e histórico de
métricas, backed by Postgres. Sem dashboard/UI de dados (ver
@herald/dashboard, congelado desde a revisão que decidiu deixar a parte
visual de lado por agora — continua existindo, separado, pro caso de uso original de fazer
poll de /metrics de Gateways individuais) — mas serve uma tela HTML mínima de
autorização (GET /auth/device, ver seção "Autenticação" abaixo), categoria diferente de
um dashboard de métricas: um formulário funcional de login, sem framework/build step.
Três formas de falar com este pacote, cada uma pra um consumidor diferente:
- HTTP autenticado (
/api/outposts/*,/api/users/*) —@heraldserver/clifala com isso viaBearer <token>(ver "Autenticação" abaixo). Nenhum operador humano tem credencial de Postgres — só o processoserverfala com o banco. - HTTP de push (
POST /api/outposts/reports) — o Gateway/app monitorada usa isso pra empurrar métricas (@heraldserver/outpost'screateOutpostReporter,HERALD_SERVER_URL), autenticado só pela Outpost key (não por usuário). - Biblioteca (
@heraldserver/server, import direto) —PgOutpostStore,PgReportsStore,createPool,migrate,SCHEMA_SQL(src/lib.ts). Uso interno (o próprioserver.ts) e pra quem quiser embutir o control plane noutro processo.
Autenticação
Multi-usuário — substitui o acesso direto a Postgres que @heraldserver/cli fazia antes
(ver ARCHITECTURE.md §4.7). Dois jeitos de autenticar:
- Bypass de loopback — requests com origem
127.0.0.1/::1/::ffff:127.0.0.1(IP real da conexão TCP, nunca um header) são liberadas semAuthorization, com um usuário sintético de roleadmin. Mesmo raciocínio de confiança do Postgres (pg_hba.confpeer/trust local) e do daemon do Docker: quem tem acesso de shell na máquina que rodaserverjá está confiado. Resultado prático: operador sozinho,heraldrodando na MESMA máquina queherald-server, nunca precisa deherald login. Cuidado: só é seguro seservernão estiver atrás de reverse proxy também em loopback (nesse caso todo tráfego proxied chegaria como127.0.0.1, inclusive de fora). Nesse cenário, desligue comHERALD_DISABLE_LOOPBACK_TRUST=1ou configuretrust proxy+ valideX-Forwarded-Forno proxy. herald login(device flow, tipogh auth login) — pra acesso remoto (--server-urlapontando pra um host que não é a própria máquina). CLI pede um código emPOST /api/auth/device/code, abre (ou imprime) uma URL — humano autentica numa tela HTML servida por este processo (GET /auth/device,POST /auth/device/verify, sem framework/JS obrigatório, form HTML puro), CLI faz poll emPOST /api/auth/device/tokenaté virar autorizado. RFC 8628 (Device Authorization Grant) adaptado pra autenticação própria em vez de OAuth de terceiro — sem IdP externo, é o próprioserverque autentica contra a tabelausers.
Bootstrap do primeiro admin: no boot, se users estiver vazia E
HERALD_ADMIN_EMAIL/HERALD_ADMIN_PASSWORD estiverem setadas, cria esse admin
automaticamente (idempotente — só roda enquanto a tabela tá vazia). Sem isso, o primeiro
admin teria que ser inserido manualmente no banco.
Papéis: admin (usuários + Outposts) e member (só Outposts) — sem ACL por Outpost
individual (decisão consciente de simplicidade, não lacuna escondida).
Convite de usuário por link (herald user invite, admin-only) — alternativa a herald
user create (que exige o admin escolher e relayar uma senha temporária de algum jeito
inseguro). POST /api/users/invite gera um link de uso único, válido por 7 dias
(GET /invite/:token) — o convidado define a PRÓPRIA senha numa tela HTML, e a própria
página já mostra o comando herald login --server-url ... pronto (resolve também a
descoberta da URL do server, embutida no link). Herald nunca manda o link por email
sozinho — quem convida compartilha por qualquer canal (Slack, WhatsApp, email próprio).
Por que Postgres, e por que isso agora exige Docker
Antes, tudo era self-host "zero infra": Outpost persistido em arquivo JSON, métricas em memória. Isso resolvia identidade (sobrevive a restart) mas não histórico (métricas zeravam a cada restart do Dashboard). Trocar pra Postgres resolve os dois de verdade — o trade-off consciente é que agora rodar isso exige uma instância Postgres de verdade, mais fácil via container.
Instalação
npm install
npm run buildBanco de dados
docker compose up -d # sobe Postgres local (postgres:16-alpine)DATABASE_URL esperado (já é o default do docker-compose.yml deste pacote):
postgres://herald:herald@localhost:5432/herald_serverSem DATABASE_URL, o processo recusa subir (erro claro, sem fallback silencioso —
diferente do resto da config, que sempre teve default local).
Schema é aplicado automaticamente no startup (CREATE TABLE/INDEX IF NOT EXISTS,
idempotente — sem framework de migração, ver src/schema.ts).
Rodando
Instalado do npm (uso real, fora deste monorepo):
npm install -g @heraldserver/server
DATABASE_URL=postgres://herald:herald@localhost:5432/herald_server \
[email protected] HERALD_ADMIN_PASSWORD=troque-isso \
herald-server
# Herald Server rodando em http://localhost:4810HERALD_ADMIN_EMAIL/HERALD_ADMIN_PASSWORD são opcionais — sem elas, ninguém consegue
herald login até um admin existir (mas o bypass de loopback continua funcionando pra
quem estiver na mesma máquina, ver "Autenticação" acima).
Ou sem instalar global: npx @heraldserver/server. Dentro deste monorepo (contribuindo):
DATABASE_URL=postgres://herald:herald@localhost:5432/herald_server npm startPorta default 4810 — faixa 48xx reservada pros apps ativos do Herald (server=4810,
poc=4811), incomum o bastante pra não colidir com outra ferramenta rodando na mesma
máquina. @herald/dashboard (congelado) fica em 4000, fora dessa faixa.
Observabilidade (Prometheus)
docker compose up -d prometheus # sobe junto com o Postgres, porta 9090prometheus.yml já vem configurado pra fazer scrape de http://host.docker.internal:4811/metrics
(o /metrics de um app usando @heraldserver/gateway, ex: poc/) — endereço portável,
funciona em Docker Desktop (Mac/Windows) e em Linux com dockerd nativo
(extra_hosts: host.docker.internal:host-gateway no docker-compose.yml).
Status atual: MetricsCollector baseado em prom-client implementado —
@heraldserver/prometheus.
/metrics só devolve texto Prometheus de verdade se a app configurar
PrometheusMetricsCollector explicitamente (default continua
InMemoryMetricsCollector/JSON — ver README do pacote). Na PoC, isso é
HERALD_METRICS=prometheus.
Gotcha conhecido: em WSL2 + Docker Desktop rodando o app monitorado fora de
container (fluxo de dev atual — node dist/server.js direto na distro), o container do
Prometheus não alcança host.docker.internal (resolve pro gateway da VM do Docker
Desktop, não pra rede da distro WSL2). Não é bug deste repo — é limitação dessa topologia
específica de máquina. Funciona normal em servidor Linux real ou Docker Desktop sem WSL2
no meio.
Testes
Testes rodam contra Postgres real (não pg-mem/mock), cada arquivo de teste cria seu
próprio banco efêmero (herald_test_<random>) e derruba no final — precisa do
docker compose up -d deste pacote rodando antes:
docker compose up -d
DATABASE_URL=postgres://herald:herald@localhost:5432/herald_server npm run build && npm testEndpoints
| Rota | Auth | Descrição |
|---|---|---|
| POST /api/outposts/reports | Outpost key (Bearer) | Push de métricas. 401 (key errada/desconhecida) ou 403 {error: "outpost_stopped"} (key válida, Outpost pausado) |
| POST /api/auth/login | — | Login direto (email/senha) — usado pela tela HTML do device flow, não pelo CLI |
| POST /api/auth/device/code | — | CLI inicia o device flow — retorna deviceCode/userCode/verificationUriComplete |
| GET /auth/device | — | Tela HTML — humano autoriza o user_code aqui |
| POST /auth/device/verify | — | Form POST da tela acima (email/senha) |
| POST /api/auth/device/token | — | CLI faz poll aqui até o deviceCode virar autorizado |
| POST /api/outposts · GET /api/outposts · GET/DELETE /api/outposts/:id · POST /api/outposts/:id/{stop,start} · POST /api/outposts/prune | usuário (Bearer) ou loopback | CRUD de Outpost — admin e member |
| POST /api/users · GET /api/users · DELETE /api/users/:id | usuário admin (Bearer) ou loopback | Gestão de usuários — admin-only |
| POST /api/users/invite | usuário admin (Bearer) ou loopback | Gera link de convite — retorna {inviteUrl, expiresAt} |
| GET /invite/:token | — | Tela HTML — convidado define a própria senha |
| POST /invite/:token/accept | — | Form POST da tela acima (password+confirmPassword) — cria o usuário |
Biblioteca (@heraldserver/server)
import { createPool, migrate, PgOutpostStore, PgReportsStore } from "@heraldserver/server";
const pool = createPool(databaseUrl);
await migrate(pool); // idempotente, seguro de rodar toda vez
const outposts = new PgOutpostStore(pool);
const reports = new PgReportsStore(pool);src/index.ts (o processo HTTP, npm start) não faz parte desse main/types — só é
invocado via node dist/index.js, tem efeito colateral (app.listen).
Limitações conhecidas
- Sem retenção automática —
outpost_reportsé append-only por padrão.herald outpost prune [<id>] --older-than-days <n>poda manualmente (tipodocker system prune, sem cron/job rodando sozinho — decisão consciente, ver TESTPLAN.md §5). Fica com o operador lembrar de rodar; não acontece sozinho. - Geração de id/nome/chave é duplicada verbatim de
dashboard/src/outposts.ts(dashboard está congelado, não deve virar dependência de ninguém) — mudança de segurança nessa lógica precisa ser replicada nos dois lugares à mão. Auditado em 2026-08-08 (diffbyte-a-byte, versrc/outposts.ts): idênticos hoje — risco aceito e registrado, não eliminado.
