@heraldserver/proxy
v0.1.0
Published
Reverse-proxy standalone de referência para o Herald Protocol — aplica identificação, política, negociação de formato e métricas na frente de qualquer aplicação HTTP (qualquer linguagem), sem exigir mudança de código na app.
Maintainers
Readme
@heraldserver/proxy
Reverse-proxy standalone de referência para o Herald Protocol — aplica identificação
de agentes, Policy Engine, negociação de formato, rate limiting e métricas na frente de
qualquer aplicação HTTP, sem exigir mudança de código nela. Diferente de
@heraldserver/gateway (middleware Express, só funciona em apps Node que
instalam o pacote e editam o próprio código), o proxy fica na frente — funciona com
qualquer stack que fale HTTP (PHP, Python, Ruby, Java, .NET, WordPress, etc.).
Reusa @heraldserver/sdk (identificação/política/negociação/assinatura/monetização/
métricas — tudo função pura sobre headers+config, nada acoplado a Express) e
@heraldserver/outpost (push de métricas) sem nenhuma mudança nesses dois pacotes, nem em
@heraldserver/server/@heraldserver/cli — um Outpost não sabe (nem precisa saber) se
quem está empurrando métricas é um middleware Node ou este proxy.
Instalação e uso
npm install -g @heraldserver/proxy// herald.config.mjs
export default {
upstreamOrigin: "http://localhost:3000", // sua app já rodando aqui
discovery: {
origin: "https://minhaapp.com",
defaultPolicy: { read: "allow", train: "deny" },
},
policy: {
default: { read: "allow", train: "deny" },
byAgentType: {
crawler: { read: "allow", train: "ask", rateLimit: { requests: 10, windowSeconds: 60 } },
},
},
};herald-proxy start --config herald.config.mjs --port 8080
# Herald Proxy escutando em http://0.0.0.0:8080, encaminhando para http://localhost:3000--port/--upstream na linha de comando (ou PORT/UPSTREAM_ORIGIN no ambiente)
sobrepõem o que estiver no arquivo. HERALD_SERVER_URL+HERALD_OUTPOST_KEY no ambiente
populam config.reporting sem precisar declarar no arquivo.
Aponte seu load balancer/DNS pro proxy em vez de direto na app (upstreamOrigin) — TLS
fica por conta de outra camada na frente (nginx/Caddy/load balancer da nuvem), o proxy
fala HTTP puro (mesma postura de assertSecureServerUrl no resto do projeto: loopback/rede
privada ok, HTTP público não).
Transform hook (opcional) — structured-json de verdade, sem o proxy adivinhar nada
Sem hook configurado, o proxy só repassa a resposta original da app (HTML) — nunca tenta
raspar/reescrever conteúdo (ARCHITECTURE.md §4.3: seria frágil e violaria "conteúdo
inalterado", RFC-0001 §7). discovery.capabilities, se não informado, é derivado
automaticamente como ["html", ...formatos com hook configurado] — o proxy nunca anuncia
uma capacidade que não consegue de fato entregar.
Pra structured-json de verdade, a app expõe um endpoint HTTP próprio (qualquer
linguagem):
// herald.config.mjs
export default {
upstreamOrigin: "http://localhost:3000",
transformHooks: [
{ pattern: "/artigos/*", format: "structured-json", transformUrl: "http://localhost:3000/_herald/transform" },
],
discovery: { origin: "https://minhaapp.com", defaultPolicy: { read: "allow" } },
policy: { default: { read: "allow" } },
};O proxy faz POST nesse endpoint com { method, path, query } — nunca o corpo já
renderizado (a app regenera da própria fonte de dados, mesma fonte que gera o HTML) — e
devolve a resposta pro agente. Endpoint fora do ar/timeout/non-2xx → 502 explícito (nunca
cai silenciosamente pro HTML original: trocar formato sem avisar seria pior surpresa que
um erro claro).
Exemplo mínimo do lado da app (qualquer linguagem, aqui em Node só de exemplo):
app.post("/_herald/transform", (req, res) => {
const slug = req.body.path.replace(/^\/artigos\//, "");
const artigo = getArtigoPorSlug(slug); // mesma fonte de dados do HTML
res.json({ title: artigo.title, body: artigo.body, publishedAt: artigo.publishedAt });
});Headers de decisão repassados pro upstream
Diferente do Gateway in-process (getHeraldContext(req), só existe porque middleware e
app compartilham o mesmo processo), o proxy repassa a decisão via headers na requisição de
saída — não normativos (RFC-0001 não define esse sentido), mesmo tratamento que
X-Herald-Debug-Agent-Verified já recebe no Gateway hoje:
| Header (proxy → upstream) | Conteúdo |
|---|---|
| X-Herald-Agent-Id | agent.agentId (vazio se não identificado) |
| X-Herald-Agent-Type | agent.agentType |
| X-Herald-Policy-Decision | mesmo formato de Herald-Policy-Decision (resposta, RFC-0001 §8.4) |
Desligar com forwardDecisionHeaders: false na config.
Limitações conhecidas (v1)
- Sem WebSocket — só request/response HTTP comum. Público-alvo inicial: sites de conteúdo/API, não apps de chat em tempo real.
- Sem retry se o upstream estiver fora do ar — uma tentativa,
502direto (retry traria complexidade de idempotência em métodos não-GET). - Rate limit in-memory por processo (
FixedWindowRateLimiter, mesma limitação do Gateway) — múltiplas instâncias do proxy não compartilham contador. Mais provável de bater aqui que no Gateway in-process, já que escalar horizontalmente um proxy na frente de uma app é mais natural. - TLS: proxy não termina HTTPS — espera estar atrás de outra camada (nginx/Caddy/load balancer).
Testes
npm run build
npm test # node --test dist/**/*.test.jsMesma filosofia de gateway/src/gateway.test.ts: node:test + supertest contra
proxy.requestListener (não precisa de porta real pro proxy em si), upstream real
(node:http.createServer numa porta efêmera) — nunca mock do pipeline. Cobre pass-through,
deny/ask/rate-limit curto-circuitando sem chamar upstream, monetização chamando o upstream
de verdade após pagamento liquidado, dispatch de transform hook (upstream real não
chamado), fallback pra forward quando não há hook, upstream fora do ar → 502, e os headers
de decisão chegando no upstream.
Status
Cumpre o item já previsto em ARCHITECTURE.md §4.1 ("permanece como opção para Fase 3
avançada") — implementação de referência, não obrigatória: quem só tem app Node/Express
continua podendo usar @heraldserver/gateway (zero salto de rede extra); quem tem
qualquer outra stack, ou não quer/pode editar código da app, usa este pacote.
