Go and Node middleware

Check every request inside your Go or Node app with /v1/gateway/check, including body digests for signed agents.

When there is no nginx in front of your app, or a route needs its body checked, run the gateway check inside the application. The repository has two small, dependency-free adapters with tests: examples/go-middleware (Go net/http) and examples/express (Node 18+, Express or plain node:http). Copy the one you need into your project.

Both call GET /v1/gateway/check with the backend secret, verify a signed agent's Content-Digest against the body they read (sha-256 or sha-512) and send X-Original-Body-Digest, strip client-supplied X-AgentGate-* headers, fail closed on errors unless told otherwise, and include a /v1/siteverify helper for form posts.

Go

Go
gate := &agentgate.Client{
	Endpoint:   "http://127.0.0.1:18080",          // private AgentGate address
	Credential: os.Getenv("AGENTGATE_CREDENTIAL"), // the site's backend secret, ags_…
}
http.Handle("/api/", gate.Middleware(apiHandler))

// In a handler: the decision.
d, ok := agentgate.FromContext(r.Context()) // d.Decision, d.Reason, d.Agent, d.WouldDecision

Node

JavaScript
const express = require("express");
const { agentgate, siteVerify } = require("./agentgate");

const opts = { endpoint: "http://127.0.0.1:18080", credential: process.env.AGENTGATE_CREDENTIAL };
const app = express();
app.use("/api", agentgate(opts)); // before body parsers, or after express.raw()
app.post("/api/orders", (req, res) => {
  // req.body is the raw Buffer; req.agentgate is the decision
  res.json({ ok: true, agent: req.headers["x-agentgate-agent"] || null });
});

Behaviour

AgentGate answersThe middleware
204 allowruns your handler; the body is still readable; the decision is available (agentgate.FromContext in Go, req.agentgate in Node) and the request carries X-AgentGate-Decision-ID, X-AgentGate-Agent (verified agent) and X-AgentGate-Would-Decision (monitor mode)
401 challengeanswers 401 with {"error": "…", "reason": "<code>"}
403 blockanswers 403, or 429 when the reason is rate_limited
error or timeoutanswers 503 (fail closed), unless FailOpen: true / failOpen: true

Options: the site key, the maximum body size (default 1 MiB; larger bodies get 413 without a check), the timeout (2 s by default: Go HTTPClient, Node timeoutMs), and, behind a proxy, functions for the client IP and protocol the proxy vouches for (Go ClientIP / Proto, Node clientIP(req) / proto(req)).

Form posts

A hidden agentgate_token field is in the body, so validate it in the route with the helper:

Go

Go
res, err := gate.SiteVerify(ctx, agentgate.SiteVerifyRequest{
	Token: r.FormValue("agentgate_token"), ExpectedAction: "signup",
	ExpectedOrigin: "https://app.example.com", RequestID: operationID,
})
if err != nil { /* AgentGate unavailable: fail closed */ }
if !res.Success { /* res.Error: invalid_token, token_used, … */ }

Node

JavaScript
const r = await siteVerify(opts, { token: req.body.agentgate_token, expectedAction: "signup",
  expectedOrigin: "https://app.example.com", requestId: operationId });
if (!r.success) return res.status(403).json({ error: r.error }); // throws when AgentGate is down

Tests

  • Go: go test ./examples/go-middleware/... (mocked AgentGate) and go test -run TestGoMiddlewareAgainstGateway . (the real endpoint).
  • Node: cd examples/express && node --test (mocked AgentGate).

View as Markdown