@delegus/sdk
v0.3.3
Published
Delegus SDK and CLI (spec §15 step 4): org.create, agent.create, grant.create, agent.sign, delegus.verify, outcome reporting.
Readme
@delegus/sdk
Spec §15 step 4: the three roles of the protocol as one small library, plus
the delegus CLI. Zero runtime dependencies beyond @delegus/core; Node ≥
22.18 (type stripping, no build step). Signers are pluggable (§2.2): the
in-memory Ed25519 signer is the dev default; anything that produces a 64-byte
Ed25519 signature over raw bytes (KMS, HSM, TPM) fits the Signer interface.
Relying Party: the afternoon integration
import { Delegus, actions } from "@delegus/sdk";
const delegus = new Delegus({ apiKey: process.env.DELEGUS_API_KEY, publicBaseUrl: "https://seller.example" });
app.post("/orders", async (req, res) => {
const d = await delegus.verify({
grant: req.header("Delegus-Grant"),
proof: req.header("Delegus-Proof"),
action: actions.purchase({ resource: req.body.orderId, amount: req.body.amountMinor, currency: req.body.currency }),
request: { method: req.method, path: req.originalUrl }, // optional §4.3 binding check
});
if (d.decision !== "ALLOW") return res.status(403).json({ reason: d.reason });
// proceed; persist d.receipt (the signed JWS) with the order
});verify() returns the signed Receipt for ALLOW and DENY alike (§5.6). With
request given, the SDK first checks the Proof's htm/htu against the
request the RP actually received (publicBaseUrl + path, normalized per
§4.3) and throws ProofBindingError on mismatch, so a Proof made for one
endpoint cannot be presented at another.
Delegus answers with the receipt of the relying party's own profile: a
delegus-receipt-v1 for a v0.2 relying party, a delegus-receipt-v2 (with
authority, authority_opening, relies_on and opening beside it) for one
on the v0.3 profile (delegus-base-v3). The default, protocol: "auto", takes
either: it reads the version from inside the signed receipt, checks a v1
receipt's signature under the service's published keys and returns it as
before, and checks a v2 response in full (signature, the authority opened
against its commitment, every hash and opening), failing closed on anything
else. Receipt keys come from the service DID document
(/.well-known/did.json, did:web:delegus.ai in production), fetched once
per client and cached; if they cannot be fetched, verify() throws. isVerifyResponseV3(d) tells the two apart. requires (the
prior decisions an action must rest on) is for v0.3 relying parties only; a v1
receipt in answer to it is refused. protocol: "v0.2" and protocol: "v0.3"
pin one profile: a receipt of the other one throws ResponseIntegrityError
naming the pin and the receipt version received. Later:
delegus.outcomes.report(receiptId, { type: "fulfilled", occurred_at }) and
delegus.decisions.get(receiptId).
Agent
import { Agent, actions } from "@delegus/sdk";
const agent = Agent.create(); // did:key, in-memory Ed25519 (or new Agent(customSigner))
// … the Principal issues a Grant naming agent.did …
const headers = await agent.headers({ grant: grantJws, audience: rpDid, method: "POST", url: "https://seller.example/orders", action });
await fetch("https://seller.example/orders", { method: "POST", headers: { ...headers, "content-type": "application/json" }, body });Both sides build the Action with actions.purchase() / actions.apiCall()
from the same wire fields, which is what makes action_hash and
request_hash agree (§4.3).
Principal
const admin = new Delegus({ adminToken });
const org = await admin.org.create({ slug: "acme" }); // managed: key generated locally, public key registered
const grant = await org.principal.grant.create({
agent: agent.did,
authority: [{ action: "commerce:purchase", constraints: { maxAmount: 50000, currency: "USD" } }],
}); // allocates the status entry at Delegus, signs locally
await org.principal.grant.revoke(grant.id); // resolves once every later /verify anywhere will deny
await org.principal.agents.disable(agent.did); // kill switch
await org.principal.keys.compromise(org.kid); // §11
const hosted = await admin.org.register({ did: "did:web:acme.com", kid: "did:web:acme.com#key-1", signer });
hosted.verification; // DNS TXT / well-known instructions
await hosted.principal.verifyDomain();Organizations (dashboards, consoles)
An organization is a customer account that owns Principals and relying
parties. Org keys (dk_org_read_…, dk_org_admin_…) read everything about
the members and, with role admin, administer them. They are never accepted
by verify or by grant.create. A console holds one org key per tenant
server-side and keeps its own users out of this API.
const admin = new Delegus({ adminToken });
const org = await admin.orgs.create({ slug: "acme", name: "Acme Inc" });
await admin.orgs.addMember(org.org_id, "did:web:acme.com");
const { api_key } = await admin.orgs.apiKeys.create(org.org_id, "admin", "console tenant");
const tenant = new Delegus({ apiKey: api_key });
await tenant.orgs.get(org.org_id); // members with verification state and RP settings
await tenant.decisions.list({ keyId: "9d44dd608cfe" }); // receipts across the org's relying parties
await tenant.usage({ byKey: true }); // verifies per day, per RP API key
await tenant.grants.list({ state: "active" });
await tenant.audit({ action: "grant.revoke" });
await tenant.apiKeys.create("seller prod", sellerDid); // mint a member's verify key (shown once)
await tenant.orgs.principal("did:web:acme.com").grant.revoke(grantId); // act for a member PrincipalMCP server (Policy Enforcement Point)
Make a Node (Express-style) MCP server authorization-aware in one line. Every JSON-RPC tools/call
is verified; nothing runs without an ALLOW, and each verified call returns a
signed Decision Receipt you keep. Fail closed. initialize, tools/list,
ping and notifications pass through.
Install
npm install @delegus/sdkOne line
import express from "express";
import { Delegus } from "@delegus/sdk";
const app = express();
app.use(express.json());
const delegus = new Delegus({ apiKey: process.env.DELEGUS_RP_KEY, publicBaseUrl: "https://tools.acme.com" });
app.use("/mcp", delegus.mcp()); // every tools/call is now verifiedOn ALLOW the receipt is on req.delegus.receipt and Delegus-Receipt-Id is
set; on DENY the middleware returns HTTP 403 with error.data.delegus = {
decision, reason, receipt_id, receipt_url } (JSON-RPC error code -32040) and
the tool never runs. A missing-credentials response adds Delegus-Relying-Party
and Delegus-Verify: required for discovery. Options: mcp({ tools, onDecision,
relyingParty, protocol, requires, map }) — tools limits enforcement to an
allow-list or predicate (unlisted tools pass through unverified). protocol
is passed to verify(): the default "auto" works against a relying party on
either profile, and for one on the v0.3 profile (delegus-base-v3)
req.delegus.receipt is the checked v0.3 response (the receipt with
authority opened against authority_commitment). requires lists prior
decisions every enforced call must rest on (v0.3 relying parties only).
"v0.2" and "v0.3" pin one profile; a receipt the setting does not accept
refuses the call with SERVICE_UNAVAILABLE (the cause is in onDecision's
error), and the middleware never retries a check.
Anything the middleware cannot read is refused with the same 403, never passed
through: a POST with no parsed body (mount express.json() before it), a body
that is not JSON or not a JSON-RPC message, or that has duplicate member
names, names that differ only by case, or an integer above 2^53 − 1
(REQUEST_UNREADABLE; the server behind could read a different value than
the one checked), a JSON-RPC
batch that carries an enforced tools/call (BATCH_NOT_SUPPORTED; batches
left MCP in 2025-06-18), and a tools/call without a string tool name
(TOOL_NAME_INVALID). These are the middleware's own refusals, like a missing
Delegus-Grant: Delegus is never called, so they carry no signed receipt
(receipt_id is null). A batch passes only when every message in it provably
invokes no enforced tool. A raw string or byte body is parsed and enforced;
hand your MCP server the same parsed body the middleware saw rather than
re-parsing the raw bytes, so both read one message. GET (the SSE stream),
HEAD, OPTIONS, a DELETE without a body (session end) and JSON-RPC responses
pass; a body on any other verb is read like a POST.
The agent side
The agent presents two headers on the tools/call POST, bound to the same tool URI:
const headers = await agent.mcpHeaders({ grant, endpoint: "https://tools.acme.com/mcp", tool: "get_customer", rpDid });
// { "Delegus-Grant": "eyJ…", "Delegus-Proof": "eyJ…" }When the server publishes a tool map (below), the agent fetches it once and signs through it, so it builds the same Action the server checks:
import { fetchToolMap } from "@delegus/sdk";
const found = await fetchToolMap("https://tools.acme.com/mcp"); // cached by hash; null when the server publishes none
const headers = await agent.mcpHeaders({ grant, endpoint: "https://tools.acme.com/mcp", tool: "git.push", rpDid,
args: { branch: "feature/login" }, ...(found ? { map: found.map } : {}) });What it covers: the tool, and with a map its arguments
Without a map the middleware covers the tool: each tools/call is one
api:call on the tool URI <publicBaseUrl><path>/tools/<name>, so a Grant's
resources decide which tools an agent may call, not what it calls them
with. A tool map says how a tool's arguments become the Action's resource
and amount, and the Grant can then constrain them:
app.use("/mcp", delegus.mcp({ map: {
version: 1,
tools: {
"git.push": { action: "api:call", resource: "refs/heads/{arg:/branch}" },
"payment.send": { action: "commerce:purchase", resource: "payee:{arg:/recipient}",
amount: { value: "/amount_cents", currency: "/currency", unit: "minor" } },
},
} }));With that map, git.push with { branch: "feature/x" } is an api:call on
<tool URI>/refs/heads/feature/x, so a Grant resource pattern
https://tools.acme.com/mcp/tools/git.push/refs/heads/feature/* allows
feature branches and nothing else, and payment.send with
{ recipient: "acme", amount_cents: 2400000, currency: "USD" } is a
commerce:purchase of 2,400,000 minor units on payee:acme, refused with
AMOUNT_EXCEEDS_AUTHORITY above the Grant's maxAmount. The receipt pins
exactly the mapped values.
- Placeholders are
{arg:<JSON pointer>}into the call'sarguments(RFC 6901, not RFC 6570); values are strings, or numbers and booleans in their JSON text. A value is split on/and each piece percent-encoded with only unreserved characters kept, so a value can never add a wildcard, climb with..(plain, percent-encoded or in lookalike characters) or change the tool URI, while/inside a value keeps its meaning. Write resource patterns in NFC; values are not normalized. actionisapi:call(the resource is appended to the tool URI) or one ofcommerce:purchase,commerce:quote,commerce:accept,commerce:fulfil(the resource is the template's result; the priced three needamount:unitminor, a non-negative integer, ormajorwithdecimals, a plain decimal such as"1.50"converted exactly and refused, never rounded, when it has more fraction digits).currencyis a pointer or an ISO 4217 code.- Tools without an entry keep the tool-URI Action. A map the middleware
cannot apply throws when the middleware is created (
ToolMapErrornames the tool and field). A call whose arguments the map cannot resolve is refused before Delegus is asked:ARGUMENTS_UNMAPPABLE, the middleware's own refusal likeMISSING_CREDENTIALS(no receipt), with the field named inerror.data.delegus.hintandonDecision'serror. - Agents must build the same Action. The middleware serves the map at
<path>/delegus-tools.json(withDelegus-Tools-Hash) and names it in aDelegus-Tools: <url>; sha256=<hash>header when credentials are missing and when an agent signed for a mapped tool without the map (PROOF_ACTION_MISMATCH, with the hint to update the SDK or fetch the map). - Two tools mapped to the same commerce type with the same resource shape
share one authority: a Grant for one authorizes the other
(
toolMapWarnings(map)lists such pairs). Only the fields a template names are bound;git.push'sforce: trueis still authorized by afeature/*Grant, so validate it in the tool.
The shared vector set @delegus/conformance mcp/tool-map.json (src/mcp/tool-map.json in the repository) pins
every rule; the Python middleware (delegus-mcp) and the gateway apply the
same map.
Ordinary HTTP APIs (REST, any language)
Banks and suppliers run REST, not MCP. The same check sits in front of an HTTP API: the agent signs for the exact method and URL, and a route map says how a request becomes the Action Delegus checks.
import { Delegus } from "@delegus/sdk";
const delegus = new Delegus({ apiKey: process.env.DELEGUS_RP_KEY!, publicBaseUrl: "https://pay.example.com" });
// Express style
app.use(express.json());
app.use(delegus.http({ routeMap }));
// Fastify
import { fastifyDelegusHttp } from "@delegus/sdk";
fastify.addHook("preHandler", fastifyDelegusHttp(delegus, { routeMap }));{ "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 pointers are the tool map's, resolved against { body, path,
query, url }. A money route becomes commerce:purchase so the Grant can say
payee:acme-* up to maxAmount; an api:call route binds the request URL
itself, so the Grant names https://pay.example.com/accounts/*. A request
matching no route is still checked as api:call on its URL; only OPTIONS
passes unchecked. On ALLOW the handler runs with req.delegus = { receipt,
action, route } and the response carries Delegus-Receipt-Id; on DENY the
caller gets 403 with { error, message, delegus: { decision, reason,
receipt_id, receipt_url } } and the handler never runs. A Proof signed for
another method or URL is PROOF_BINDING_MISMATCH before Delegus is asked.
Missing credentials: 401 with Delegus-Verify: required,
Delegus-Relying-Party and Delegus-Routes: <origin>/delegus-routes.json;
sha256=…; the middleware serves the map at that path.
The agent side builds the same Action from the same map:
const headers = await agent.httpHeaders({ grant, method: "POST", url: "https://pay.example.com/payments", rpDid, body, routes: routeMap });
await fetch("https://pay.example.com/payments", { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify(body) });For an API in another language, @delegus/http-gateway runs this check as a
reverse proxy in front of it.
Anyone checking the archive
Every ended UTC day gets a signed Merkle root over the receipt ids Delegus issued that day (RFC 6962, SHA-256), chained to the previous day and signed with the status key in the service DID document.
const d = new Delegus(); // no key needed
await d.checkpoints.verify("2026-09-15"); // recompute the root from the published leaves, verify the signature
await d.receipts.inclusion("drc_01K4Q7ZP9X2M3N4R5S6T7V8W9Y"); // audit path for one receipt
await d.checkpoints.contains("2026-09-15", myReceiptIds); // are my ids leaves of that day?CLI: delegus checkpoint verify --day 2026-09-15, delegus receipt inclusion --id drc_…,
delegus checkpoint contains --day … --ids ….
Anyone holding a receipt
const keys = await delegus.serviceKeys(); // from the service DID document
delegus.receipts.verify(receipt.receipt, keys, "did:web:delegus.ai");
await delegus.receipts.reverify({ receipt, grant, proof, action }); // fetches artifacts by hash from /evidence, re-runs the open checks (v0.2 and v0.3) with now = evaluated_at; r.notReproduced names what needs entitled materialCLI
delegus --help. Environment: DELEGUS_API_URL, DELEGUS_API_KEY,
DELEGUS_ADMIN_TOKEN. Keys live in Ed25519 private JWK files (mode 0600).
Commands: keygen, org init|show|add-member|key-create|key-list|key-revoke,
org create|register|verify, rp create, agent create|register|disable,
grant create|revoke, key list|create|revoke|retire|compromise,
proof sign, verify, outcome report, decision get, receipt verify|inclusion,
checkpoint verify|contains, action purchase|api-call, doctor. verify exits 0 on ALLOW and 2 on
DENY. org init creates an organization account; org create creates a
managed Principal (did:web:<domain>:org:<slug>), optionally inside one
(--org).
Tests
npm --workspace packages/sdk test runs the SDK against the in-process API
from apps/api's test harness (memory adapters, fixed clock) and the CLI as
a subprocess.
License
Apache-2.0. See LICENSE. The Delegus service (apps/api) is not part of this package and is not open source.
