Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
spec/spec.md
spec/fixtures/
spec/normative-ledger.json
32 changes: 17 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# @proof.com/x401-node - AI Assistant Guide

ESM TypeScript library implementing the x401 protocol (https://x401.proof.com/spec):
the PROOF-REQUIRED / PROOF-PRESENTATION / PROOF-RESPONSE wire format, the Verifier Challenge,
the VP Artifact, the x401 Token / Error objects, and the OAuth Token Exchange profile.
ESM TypeScript library implementing the x401 protocol (https://x401.proof.com/spec, **v0.2.0**):
the PROOF-REQUIRED / PROOF-PRESENTATION / PROOF-RESPONSE wire format, the composed Digital
Credentials request (`presentation_requirements`), the VP Artifact (inline result or
`presentation_uri` reference), the x401 Token / Error objects, and the OAuth Token Exchange profile.

Two consumer roles, exported as namespaces:

- `agent.*` — decode PROOF-REQUIRED (header or embedded `<data>`), package a wallet result as a VP
Artifact, encode PROOF-PRESENTATION, build a token-exchange request, decode PROOF-RESPONSE errors.
- `verifier.*` — create/verify the Verifier Challenge, build/encode the payload, emit the embedded
`<data>` mirror, decode incoming VP Artifacts / Token Objects, parse token-exchange requests,
encode error objects.
- `agent.*` — decode PROOF-REQUIRED (header or embedded `<data>`), read the Verifier-composed
`presentation_requirements`, package a presentation result as a VP Artifact (inline or by
reference), encode PROOF-PRESENTATION, build a token-exchange request, decode PROOF-RESPONSE errors.
- `verifier.*` — build/encode the flat payload (carrying the caller-composed
`presentation_requirements`), emit the embedded `<data>` mirror, decode incoming VP Artifacts /
Token Objects, parse token-exchange requests, encode error objects.

Plus `createEncryptor` (AES-GCM verifier-protected nonce state).
Spec-conformance harness lives under `spec/` (pinned schema + extracted examples + normative ledger)
and `scripts/` (`sync-spec-fixtures.ts`, `extract-normative.ts`). See `spec/UPGRADING.md` for the
repeatable spec-upgrade loop and `spec/conformance.md` for the requirement→code map.

## Hard Rules

Expand Down Expand Up @@ -55,14 +59,12 @@ Plus `createEncryptor` (AES-GCM verifier-protected nonce state).

## Source Map

- `src/constants.ts` — scheme/version, header names, schema URL, token-exchange URNs.
- `src/types.ts` — wire-format types (no runtime code).
- `src/constants.ts` — scheme/version (`0.2.0`), `DC_API_PROTOCOL` (signed/unsigned), header names, schema URL, token-exchange URNs.
- `src/types.ts` — wire-format types (no runtime code): flat `X401Payload`, `DigitalCredentialRequest`, `PresentationResult`, `VPArtifact`.
- `src/encoding.ts` — base64url JSON helpers over `@owf/identity-common`; proof-header comma guard.
- `src/validate.ts` — structural validators / type guards (`X401ValidationError`).
- `src/encryptor.ts` — `createEncryptor` (AES-GCM + HKDF verifier-protected nonce state; `encrypt`/`decrypt`).
- `src/challenge.ts` — Verifier Challenge construct/verify (binds verifier id, route, method, expiry).
- `src/agent.ts` — agent-side primitives.
- `src/verifier.ts` — verifier-side primitives (re-exports challenge functions).
- `src/agent.ts` — agent-side primitives (`getDigitalCredentialRequest`, `buildVPArtifact`/`buildVPArtifactReference`, …).
- `src/verifier.ts` — verifier-side primitives (`buildPayload`, `embedHtmlData`, decoders, token-exchange parse, error builder).
- `src/index.ts` — public barrel (explicit named exports; `agent`/`verifier` namespaces).

## Publishing
Expand Down
165 changes: 70 additions & 95 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
# @proof.com/x401-node

Node.js SDK for the [x401 protocol](https://x401.proof.com/spec).
Node.js SDK for the [x401 protocol](https://x401.proof.com/spec) (v0.2.0).

x401 gates an HTTP resource behind an identity proof requirement. The server (_verifier_) returns a
[`PROOF-REQUIRED`](https://x401.proof.com/spec/#proof-header-fields) header and the user _agent_ retries
with a [`PROOF-PRESENTATION`](https://x401.proof.com/spec/#route-retry-headers) header carrying a
Verifiable Credential Presentation. This package implements the data types and processing rules for both the _verifier_ and the user _agent_.
[`PROOF-REQUIRED`](https://x401.proof.com/spec/#proof-header-fields) header carrying a composed
[Digital Credentials API](https://www.w3.org/TR/digital-credentials/) request; the user _agent_
obtains a presentation for that request and retries with a
[`PROOF-PRESENTATION`](https://x401.proof.com/spec/#route-retry-headers) header. 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`](https://www.npmjs.com/package/@proof.com/proof-vc-common). It
also does **not** build the wallet-facing OpenID4VP request; that is the user agent's responsibility.
It does **not** verify credentials — the presentation result is opaque, so pair it with a credential
library such as [`@proof.com/proof-vc-common`](https://www.npmjs.com/package/@proof.com/proof-vc-common).
It also does **not** compose or sign the OpenID4VP request, nor invoke the wallet; the verifier
authors the request (out of scope here) and this package carries it opaque in `presentation_requirements`.

## Table of Contents

- [Installation](#installation)
- [Verifier](#verifier)
- [Protect a resource (`PROOF-REQUIRED`)](#protect-a-resource-proof-required)
- [Proof challenge](#proof-challenge)
- [Proof requirement](#proof-requirement)
- [Verify a Proof (`PROOF-PRESENTATION`)](#verify-a-proof-proof-presentation)
- [Agent](#agent)
- [Read a Proof requirement (`PROOF-REQUIRED`)](#read-a-proof-requirement-proof-required)
Expand All @@ -35,120 +36,76 @@ npm install @proof.com/x401-node

### Protect a resource (`PROOF-REQUIRED`)

A protected route returns a [Proof requirement](#proof-requirement) built around a
[Proof challenge](#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](https://x401.proof.com/spec/#vp-artifact), to access the
protected resource. The challenge must follow the
[challenge format](https://x401.proof.com/spec/#verifier-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.
The [x401 payload](https://x401.proof.com/spec/#x401-payload) carries the Verifier-composed
[Digital Credentials request](https://x401.proof.com/spec/#presentation-requirements) and the OAuth
token endpoint used for [token exchange](#exchange-a-proof-for-a-token). You compose and (for the
RECOMMENDED signed mode) sign the OpenID4VP request yourself; this package carries it opaque.

```ts
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](#verify-a-proof-proof-presentation)
rejects any value whose nonce was tampered with.

##### Supply your own challenge

You can construct a [`VerifierChallenge`](https://x401.proof.com/spec/#verifier-challenge-format) if you prefer storing
the challenge server side or prefer a different nonce generation algorithm.

```ts
const challenge = {
value: `x401:${Buffer.from("https://research.example.com").toString("base64url")}:${myStoredNonce}`,
expires_at: new Date(Date.now() + 600_000).toISOString(),
};
```

#### Proof requirement
import { verifier } from "@proof.com/x401-node";

The [x401 payload](https://x401.proof.com/spec/#x401-payload) carries the challenge, the credential
query and the OAuth token endpoint used for [token exchange](#exchange-a-proof-for-a-token).

##### Create the payload

`buildPayload` requires exactly one credential query: `dcql_query` or `scope`. `oauth.token_endpoint`
is required.

```ts
const payload = verifier.buildPayload({
proof: {
challenge,
oauth: { token_endpoint: "https://research.example.com/oauth/token" },
scope: "urn:proof:params:scope:verifiable-credentials:basic",
presentationRequirements: {
requests: [
{
protocol: "openid4vp-v1-signed",
data: { request: signedOpenId4vpRequestJwt },
},
],
},
oauth: { token_endpoint: "https://research.example.com/oauth/token" },
trustEstablishment:
"https://research.example.com/.well-known/x401/trust/basic-v1",
requestId: "proof-template-basic-v1",
satisfiedRequirements: ["urn:proof:x401:satisfaction:basic:v1"],
});
```

##### Payload in the header
`protocol` is `openid4vp-v1-signed` (RECOMMENDED) or `openid4vp-v1-unsigned`, and its `data`
carries the request you composed and signed. `trustEstablishment`, `requestId`, and
`satisfiedRequirements` are optional hints.

Return the Proof requirement as a header:
Return it as a header:

```ts
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](https://x401.proof.com/spec/#embedded-proof-requirements-in-html-content).
The header remains authoritative and must still be set.
[embedded `<data>` element](https://x401.proof.com/spec/#embedded-proof-requirements-in-html-content)
(the `$schema` marker is added automatically). The header remains authoritative and must still be set.

```ts
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](https://x401.proof.com/spec/#x401-error-object) in `PROOF-RESPONSE`. See the full
Decode the artifact, then validate the presentation against the request you composed (binding,
`nonce` freshness, credential query) with your credential library and route policy. The artifact may
carry the result inline (`response`) or by reference (`presentation_uri`, which you dereference). On
failure, return an [x401 Error Object](https://x401.proof.com/spec/#x401-error-object) in
`PROOF-RESPONSE`. See the full
[verifier processing rules](https://x401.proof.com/spec/#verifier-processing-rules).

```ts
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",
});
const result = artifact.response
? artifact.response
: await fetchPresentation(artifact.presentation_uri!);

if (!check.ok) {
if (!validatePresentation(result)) {
response.setHeader(
"PROOF-RESPONSE",
verifier.encodeErrorObject(
verifier.buildErrorObject({ error: "invalid_challenge" }),
verifier.buildErrorObject({ error: "invalid_presentation" }),
),
);
return;
}

// verify artifact.vp_token with your credential library, then apply route policy
```

## Agent
Expand All @@ -157,8 +114,9 @@ See the full [agent processing rules](https://x401.proof.com/spec/#agent-process

### 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).
`detectProofRequirement` reads the header, falling back to the embedded `<data>` element.
`getDigitalCredentialRequest` returns the Verifier-composed request unmodified — pass it straight to
the Digital Credentials API (or relay it). The agent MUST NOT alter it.

```ts
import { agent } from "@proof.com/x401-node";
Expand All @@ -170,28 +128,43 @@ const requirement = agent.detectProofRequirement({
});

if (requirement) {
const nonce = agent.getNonce(requirement.payload);
const query = agent.getCredentialQuery(requirement.payload); // { scope } | { dcql_query }
const dcRequest = agent.getDigitalCredentialRequest(requirement.payload);
const result = await navigator.credentials.get({ digital: dcRequest });
}
```

If you're an intermediary relaying the request to a **remote handler** (which POSTs the result
back rather than invoking the DC API itself), add an `https` `return_uri` to the forwarded payload
with `agent.addReturnUri(payload, returnUri)`. Only a relaying intermediary sets this — never the
Verifier.

### Present a Proof (`PROOF-PRESENTATION`)

Wrap the wallet's `vp_token` in a [VP Artifact](https://x401.proof.com/spec/#vp-artifact) and retry
the same route.
Wrap the `{ protocol, data }` presentation result in a
[VP Artifact](https://x401.proof.com/spec/#vp-artifact) and retry the same route. Use the
by-reference form for results too large for a header.

```ts
const artifact = agent.buildVPArtifact({
payload: requirement.payload,
agentId: "did:web:agent.example",
vpToken,
response: result,
requestId: requirement.payload.request_id,
});

await fetch(url, {
headers: { "PROOF-PRESENTATION": agent.encodeVPArtifact(artifact) },
});
```

Or, by reference:

```ts
const artifact = agent.buildVPArtifactReference({
presentationUri:
"https://research.example.com/.well-known/x401/presentations/abc",
expiresAt: "2026-05-06T18:50:00Z",
});
```

### Exchange a Proof for a token

Exchange the artifact for a reusable Verification Token via
Expand All @@ -213,4 +186,6 @@ const tokenHeader = agent.encodeTokenObject(
await fetch(url, { headers: { "PROOF-PRESENTATION": tokenHeader } });
```

## Contributing

[Contribution guidelines for this project](CONTRIBUTING.md)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
},
"devDependencies": {
"@types/node": "^25.9.1",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"eslint": "^10.4.0",
"eslint-plugin-unused-imports": "^4.4.1",
"prettier": "^3.8.4",
Expand Down
Loading