@delegus/http-gateway
v0.1.0
Published
Delegus HTTP gateway: a reverse proxy that checks every request to an ordinary HTTP API (REST, any language) against the caller's delegated authority before forwarding it: the Grant and Proof bound to the exact method and URL, the Action built from the bo
Readme
@delegus/http-gateway
The Delegus check in front of an ordinary HTTP API, in any language. Banks and
suppliers run REST; this is @delegus/mcp-gateway for those: every request is
checked against the caller's delegated authority before the API sees it, and
only an allowed request is forwarded.
agent ──► delegus-http-gateway ──► your API (any language)
│
└─ one call to Delegus per requestRun it
DELEGUS_RP_KEY=dk_rp_… npx @delegus/http-gateway \
--upstream http://api:8080 --public-base-url https://pay.example.com --route-map routes.json--upstream: where your API listens. Keep it on a private network; the gateway protects the API only if it is the only way in.--public-base-url: the gateway's public origin, the one agents sign for. A path here is refused at startup, in words, because agents sign for<origin><request path>and a path would double it.--route-map routes.json: how a request becomes the Action Delegus checks (below). Without it, every request isapi:call <METHOD> <canonical URL>and the Grant names the URL patterns.DELEGUS_RP_KEY: a Delegus verify key, from the environment only.
The route map
{ "version": 1, "routes": [
{ "id": "payments.create", "method": "POST", "path": "/payments",
"action": "commerce:purchase",
"resource": "payee:{arg:/body/payee}",
"amount": { "value": "/body/amount", "currency": "/body/currency", "unit": "minor" } },
{ "id": "accounts.read", "method": "GET", "path": "/accounts/{id}", "action": "api:call" }
] }Templates and JSON pointers are the MCP tool map's, resolved against
{ body, path: {<param>}, query: {<name>}, url }. A money route becomes
commerce:purchase with the amount in minor units (or "unit": "major" with
decimals); the Grant then says payee:acme-* up to maxAmount. An
api:call route binds the request URL itself (its path parameters are already
in it), so the Grant names https://pay.example.com/accounts/*; a resource
template on an api:call route is refused. A request matching no route is
still checked, as api:call on its URL: nothing passes unchecked except
OPTIONS. A body the map cannot apply is refused before Delegus is asked
(ARGUMENTS_UNMAPPABLE). The map is served at /delegus-routes.json and
named in the Delegus-Routes header on a 401, so the agent builds the same
Action (Agent.httpHeaders({ routes }) in @delegus/sdk).
What happens to each request
- ALLOW: forwarded with the exact bytes that were checked, plus
Delegus-Receipt-IdandDelegus-Decision: ALLOW(a client's own copies are dropped); the client getsDelegus-Receipt-Idon the response. - DENY: 403 with
{ error, message, delegus: { decision, reason, receipt_id, receipt_url } };erroris always"DENY", the reason to act on isdelegus.reason; the API never sees the request. A Proof signed for another method or URL isPROOF_BINDING_MISMATCH(no receipt: Delegus was not asked). Missing credentials: 401 withDelegus-Verify: required,Delegus-Relying-PartyandDelegus-Routes. - Delegus unreachable: 403
SERVICE_UNAVAILABLE. It fails closed. A body that is not strict JSON or exceeds the size limit is refused (REQUEST_UNREADABLE).
In-process alternatives
Node APIs can skip the proxy: app.use(delegus.http({ routeMap })) (Express
style) or app.addHook("preHandler", fastifyDelegusHttp(delegus, { routeMap }))
from @delegus/sdk, the same check without the hop.
Limits
- The gateway must be the only way in; keep the upstream private.
- Only what the route map names is bound into the Action; validate other fields in the API.
- Bodies are read whole (default 1 MiB) so the exact bytes that were checked are the bytes forwarded; it is not a streaming proxy for uploads.
