Node.js SDK for the x401 protocol.
x401 gates an HTTP resource behind an identity proof requirement. The server (verifier) returns a
PROOF-REQUIRED header and the user agent retries
with a PROOF-PRESENTATION header carrying a
Verifiable Credential Presentation. This package implements the data types and processing rules for both the verifier and the user agent.
It does not verify credentials — the vp_token is opaque, so pair it with a credential library
such as @proof.com/proof-vc-common. It
also does not build the wallet-facing OpenID4VP request; that is the user agent's responsibility.
npm install @proof.com/x401-node
A protected route returns a Proof requirement built around a Proof challenge.
The Proof challenge contains a nonce tied to the resource the agent wants to access. The agent submits that nonce, inside a VP Artifact, to access the protected resource. The challenge must follow the challenge format. Provide your own, or use the built-in challenge encryptor to create one.
createEncryptor binds the route context into the nonce, so the verifier holds no per-challenge
state. The same secret must be present wherever challenges are verified.
import { createEncryptor, verifier } from "@proof.com/x401-node";
const encryptor = createEncryptor({ key: process.env.X401_KEY! });
const challenge = await verifier.createChallenge({
verifierId: "https://research.example.com",
resource: "https://research.example.com/papers/medical-study-123",
method: "GET",
encryptor,
ttlSeconds: 600,
});The nonce is an AES-256-GCM token (HKDF-derived key). Verify a Proof rejects any value whose nonce was tampered with.
You can construct a VerifierChallenge if you prefer storing
the challenge server side or prefer a different nonce generation algorithm.
const challenge = {
value: `x401:${Buffer.from("https://research.example.com").toString("base64url")}:${myStoredNonce}`,
expires_at: new Date(Date.now() + 600_000).toISOString(),
};The x401 payload carries the challenge, the credential query and the OAuth token endpoint used for token exchange.
buildPayload requires exactly one credential query: dcql_query or scope. oauth.token_endpoint
is required.
const payload = verifier.buildPayload({
proof: {
challenge,
oauth: { token_endpoint: "https://research.example.com/oauth/token" },
scope: "urn:proof:params:scope:verifiable-credentials:basic",
},
});Return the Proof requirement as a header:
response.setHeader("PROOF-REQUIRED", verifier.encodePayload(payload));For clients that read the body but not the headers, mirror the requirement as an
embedded <data> element.
The header remains authoritative and must still be set.
const html = `<article>…</article>${verifier.embedHtmlData(payload)}`;Decode the artifact and authenticate the challenge. Then verify vp_token with your credential
library and apply route policy. On failure, return an
x401 Error Object in PROOF-RESPONSE. See the full
verifier processing rules.
const artifact = verifier.decodeVPArtifact(
request.headers["proof-presentation"],
);
const check = await verifier.verifyChallenge({
value: artifact.challenge,
encryptor,
expectedVerifierId: "https://research.example.com",
expectedResource: "https://research.example.com/papers/medical-study-123",
expectedMethod: "GET",
});
if (!check.ok) {
response.setHeader(
"PROOF-RESPONSE",
verifier.encodeErrorObject(
verifier.buildErrorObject({ error: "invalid_challenge" }),
),
);
return;
}
// verify artifact.vp_token with your credential library, then apply route policySee the full agent processing rules.
detectProofRequirement reads the header, falling back to the embedded <data> element. Take the
nonce and credential query to build your OpenID4VP request (out of scope for this package).
import { agent } from "@proof.com/x401-node";
const res = await fetch(url);
const requirement = agent.detectProofRequirement({
headers: res.headers,
body: await res.text(),
});
if (requirement) {
const nonce = agent.getNonce(requirement.payload);
const query = agent.getCredentialQuery(requirement.payload); // { scope } | { dcql_query }
}Wrap the wallet's vp_token in a VP Artifact and retry
the same route.
const artifact = agent.buildVPArtifact({
payload: requirement.payload,
agentId: "did:web:agent.example",
vpToken,
});
await fetch(url, {
headers: { "PROOF-PRESENTATION": agent.encodeVPArtifact(artifact) },
});Exchange the artifact for a reusable Verification Token via OAuth token exchange, then present it as an x401 Token Object.
const form = agent.buildTokenExchangeForm(artifact, { resource: url });
const res = await fetch(tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form,
});
const { access_token } = agent.parseTokenExchangeResponse(await res.json());
const tokenHeader = agent.encodeTokenObject(
agent.buildTokenObject(access_token),
);
await fetch(url, { headers: { "PROOF-PRESENTATION": tokenHeader } });