Skip to content

proof/x401-node

Repository files navigation

@proof.com/x401-node

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.

Table of Contents

Installation

npm install @proof.com/x401-node

Verifier

Protect a resource (PROOF-REQUIRED)

A protected route returns a Proof requirement built around a Proof challenge.

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.

Built-in challenge encryptor

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.

Supply your own challenge

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(),
};

Proof requirement

The x401 payload carries the challenge, the credential query and the OAuth token endpoint used for token exchange.

Create the payload

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",
  },
});
Payload in the header

Return the Proof requirement as a header:

response.setHeader("PROOF-REQUIRED", verifier.encodePayload(payload));
Payload in HTML

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)}`;

Verify a Proof (PROOF-PRESENTATION)

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 policy

Agent

See the full agent processing rules.

Read a Proof requirement (PROOF-REQUIRED)

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 }
}

Present a Proof (PROOF-PRESENTATION)

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 a Proof for a token

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 } });

Contribution guidelines for this project