From 338b785ac60b6021873d384a916fd405f561915a Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Sun, 28 Jun 2026 18:28:09 -0400 Subject: [PATCH] fix: sync current x401 wire names --- README.md | 158 ++-- package.json | 1 + scripts/sync-spec-fixtures.ts | 12 +- spec/SPEC_SOURCE.json | 4 +- spec/conformance.md | 109 ++- spec/fixtures/error-object.json | 4 +- spec/fixtures/payload-1.json | 3 +- spec/fixtures/payload-2.json | 18 +- spec/fixtures/payload-3.json | 18 +- spec/fixtures/payload-4.json | 19 +- spec/fixtures/payload-5.json | 18 +- spec/fixtures/request.schema.json | 72 +- ...artifact-1.json => result-artifact-1.json} | 4 +- ...artifact-2.json => result-artifact-2.json} | 4 +- ...artifact-5.json => result-artifact-3.json} | 2 +- ...artifact-4.json => result-artifact-4.json} | 4 +- ...artifact-3.json => result-artifact-5.json} | 2 +- spec/normative-ledger.json | 95 ++- spec/spec.md | 805 +++++++++--------- src/agent.ts | 67 +- src/constants.ts | 10 +- src/index.ts | 7 +- src/types.ts | 58 +- src/validate.ts | 51 +- src/verifier.ts | 32 +- tests/spec-fixtures.test.ts | 31 +- tests/spec-schema.test.ts | 51 +- tests/x401.test.ts | 137 +-- 28 files changed, 941 insertions(+), 855 deletions(-) rename spec/fixtures/{vp-artifact-1.json => result-artifact-1.json} (57%) rename spec/fixtures/{vp-artifact-2.json => result-artifact-2.json} (57%) rename spec/fixtures/{vp-artifact-5.json => result-artifact-3.json} (52%) rename spec/fixtures/{vp-artifact-4.json => result-artifact-4.json} (57%) rename spec/fixtures/{vp-artifact-3.json => result-artifact-5.json} (52%) diff --git a/README.md b/README.md index 240da65..7c5347a 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,106 @@ # @proof.com/x401-node -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 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 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`. +Node.js SDK for the [x401 protocol](https://x401.proof.com/spec/latest/) (v0.2.0). + +x401 gates an HTTP resource behind an identity proof requirement. The server (Verifier) returns a +[`PROOF-REQUEST`](https://x401.proof.com/spec/latest/#proof-header-fields) header carrying a +composed [Digital Credentials API](https://www.w3.org/TR/digital-credentials/) request. The user +Agent obtains a credential result for that request and retries with a +[`PROOF-RESPONSE`](https://x401.proof.com/spec/latest/#route-retry-headers) header. The Verifier +reports x401-specific results and errors in +[`PROOF-RESULT`](https://x401.proof.com/spec/latest/#proof-header-fields). + +This package implements data types and wire-object processing rules for both the Verifier and the +Agent. It does not verify credentials. Pair it with a credential verification 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 a wallet. The Verifier authors the +request, and this package carries it opaque in `credential_requirements`. ## Table of Contents - [Installation](#installation) - [Verifier](#verifier) - - [Protect a resource (`PROOF-REQUIRED`)](#protect-a-resource-proof-required) - - [Verify a Proof (`PROOF-PRESENTATION`)](#verify-a-proof-proof-presentation) + - [Protect a resource (`PROOF-REQUEST`)](#protect-a-resource-proof-request) + - [Verify a result (`PROOF-RESPONSE`)](#verify-a-result-proof-response) - [Agent](#agent) - - [Read a Proof requirement (`PROOF-REQUIRED`)](#read-a-proof-requirement-proof-required) - - [Present a Proof (`PROOF-PRESENTATION`)](#present-a-proof-proof-presentation) - - [Exchange a Proof for a token](#exchange-a-proof-for-a-token) + - [Read a proof requirement (`PROOF-REQUEST`)](#read-a-proof-requirement-proof-request) + - [Present a result (`PROOF-RESPONSE`)](#present-a-result-proof-response) + - [Exchange a result for a token](#exchange-a-result-for-a-token) - [Contributing](#contributing) ## Installation -``` +```sh npm install @proof.com/x401-node ``` ## Verifier -### Protect a resource (`PROOF-REQUIRED`) +### Protect a resource (`PROOF-REQUEST`) -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. +The [x401 payload](https://x401.proof.com/spec/latest/#x401-payload) carries the +Verifier-composed credential request and the OAuth token endpoint used for +[token exchange](#exchange-a-result-for-a-token). You compose and, for the recommended signed mode, +sign the OpenID4VP request yourself. This package carries it opaque. ```ts import { verifier } from "@proof.com/x401-node"; const payload = verifier.buildPayload({ - presentationRequirements: { - requests: [ - { - protocol: "openid4vp-v1-signed", - data: { request: signedOpenId4vpRequestJwt }, - }, - ], + credentialRequirements: { + digital: { + 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"], }); ``` -`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. +`protocol` is `openid4vp-v1-signed` or `openid4vp-v1-unsigned`, and its `data` carries the request +you composed and signed. `requestId` and `satisfiedRequirements` are optional hints. Return it as a header: ```ts -response.setHeader("PROOF-REQUIRED", verifier.encodePayload(payload)); +response.setHeader("PROOF-REQUEST", verifier.encodePayload(payload)); ``` For clients that read the body but not the headers, mirror the requirement as an -[embedded `` 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. +[embedded `` element](https://x401.proof.com/spec/latest/#embedded-proof-requirements-in-html-content). +The `$schema` marker is added automatically. The header remains authoritative and must still be set. ```ts -const html = `
${verifier.embedHtmlData(payload)}`; +const html = `
...
${verifier.embedHtmlData(payload)}`; ``` -### Verify a Proof (`PROOF-PRESENTATION`) +### Verify a result (`PROOF-RESPONSE`) -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). +Decode the Result Artifact, then validate the credential result against the request you composed +with your credential library and route policy. The artifact may carry the result inline +(`credential_result`) or by reference (`credential_result_uri`, which you dereference). On failure, +return an [x401 Error Object](https://x401.proof.com/spec/latest/#x401-error-object) in +`PROOF-RESULT`. See the full +[Verifier processing rules](https://x401.proof.com/spec/latest/#verifier-processing-rules). ```ts -const artifact = verifier.decodeVPArtifact( - request.headers["proof-presentation"], +const artifact = verifier.decodeResultArtifact( + request.headers["proof-response"], ); -const result = artifact.response - ? artifact.response - : await fetchPresentation(artifact.presentation_uri!); +const result = artifact.credential_result + ? artifact.credential_result + : await fetchCredentialResult(artifact.credential_result_uri!); -if (!validatePresentation(result)) { +if (!validateCredentialResult(result)) { response.setHeader( - "PROOF-RESPONSE", + "PROOF-RESULT", verifier.encodeErrorObject( verifier.buildErrorObject({ error: "invalid_presentation" }), ), @@ -110,13 +111,13 @@ if (!validatePresentation(result)) { ## Agent -See the full [agent processing rules](https://x401.proof.com/spec/#agent-processing-rules). +See the full [Agent processing rules](https://x401.proof.com/spec/latest/#agent-processing-rules). -### Read a Proof requirement (`PROOF-REQUIRED`) +### Read a proof requirement (`PROOF-REQUEST`) `detectProofRequirement` reads the header, falling back to the embedded `` 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. +`getCredentialRequestOptions` returns the Verifier-composed credential request unmodified. Pass it +straight to the Credential Manager, or relay it. The Agent must not alter it. ```ts import { agent } from "@proof.com/x401-node"; @@ -128,48 +129,49 @@ const requirement = agent.detectProofRequirement({ }); if (requirement) { - const dcRequest = agent.getDigitalCredentialRequest(requirement.payload); - const result = await navigator.credentials.get({ digital: dcRequest }); + const credentialRequest = agent.getCredentialRequestOptions( + requirement.payload, + ); + const result = await navigator.credentials.get(credentialRequest); } ``` -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. +If you are an intermediary relaying the request to a remote handler, add an `https` `return_uri` to +the forwarded payload with `agent.addReturnUri(payload, returnUri)`. Only a relaying intermediary +sets this. The Verifier never sets it. -### Present a Proof (`PROOF-PRESENTATION`) +### Present a result (`PROOF-RESPONSE`) -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. +Wrap the `{ protocol, data }` credential result in a +[Result Artifact](https://x401.proof.com/spec/latest/#result-artifact) and retry the same route. Use +the by-reference form for results too large for a header. ```ts -const artifact = agent.buildVPArtifact({ - response: result, +const artifact = agent.buildResultArtifact({ + credentialResult: result, requestId: requirement.payload.request_id, }); await fetch(url, { - headers: { "PROOF-PRESENTATION": agent.encodeVPArtifact(artifact) }, + headers: { "PROOF-RESPONSE": agent.encodeResultArtifact(artifact) }, }); ``` Or, by reference: ```ts -const artifact = agent.buildVPArtifactReference({ - presentationUri: - "https://research.example.com/.well-known/x401/presentations/abc", +const artifact = agent.buildResultArtifactReference({ + credentialResultUri: + "https://research.example.com/.well-known/x401/results/abc", expiresAt: "2026-05-06T18:50:00Z", }); ``` -### Exchange a Proof for a token +### Exchange a result for a token Exchange the artifact for a reusable Verification Token via -[OAuth token exchange](https://x401.proof.com/spec/#oauth-token-exchange), then present it as an -x401 Token Object. +[OAuth token exchange](https://x401.proof.com/spec/latest/#oauth-token-exchange), then present it as +an x401 Token Object. ```ts const form = agent.buildTokenExchangeForm(artifact, { resource: url }); @@ -183,7 +185,7 @@ const { access_token } = agent.parseTokenExchangeResponse(await res.json()); const tokenHeader = agent.encodeTokenObject( agent.buildTokenObject(access_token), ); -await fetch(url, { headers: { "PROOF-PRESENTATION": tokenHeader } }); +await fetch(url, { headers: { "PROOF-RESPONSE": tokenHeader } }); ``` ## Contributing diff --git a/package.json b/package.json index fd2bf1d..2c003c9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "format:check": "prettier --check .", "lint": "eslint . --fix", "lint:check": "eslint .", + "prepare": "npm run build", "publint": "publint --pack npm", "test": "node --test tests/*.test.ts", "typecheck": "tsc --noEmit" diff --git a/scripts/sync-spec-fixtures.ts b/scripts/sync-spec-fixtures.ts index 0680e6e..2b71d18 100644 --- a/scripts/sync-spec-fixtures.ts +++ b/scripts/sync-spec-fixtures.ts @@ -102,7 +102,7 @@ function main(): void { }); let payloads = 0; - let vpArtifacts = 0; + let resultArtifacts = 0; let oid4vpRequests = 0; let schemaFound = false; @@ -116,7 +116,7 @@ function main(): void { write("request.schema.json", obj); schemaFound = true; } else if (obj["scheme"] === "x401") { - if (obj["presentation_requirements"] !== undefined) { + if (obj["credential_requirements"] !== undefined) { write(`payload-${++payloads}.json`, obj); } else if (obj["error"] !== undefined) { write("error-object.json", obj); @@ -124,10 +124,10 @@ function main(): void { write("token-object.json", obj); } } else if ( - obj["response"] !== undefined || - obj["presentation_uri"] !== undefined + obj["credential_result"] !== undefined || + obj["credential_result_uri"] !== undefined ) { - write(`vp-artifact-${++vpArtifacts}.json`, obj); + write(`result-artifact-${++resultArtifacts}.json`, obj); } else if (obj["response_type"] === "vp_token") { // Informative: the decoded OpenID4VP request the Verifier signs into the JAR. write(`openid4vp-request-${++oid4vpRequests}.json`, obj); @@ -148,7 +148,7 @@ function main(): void { }; writeFileSync(SOURCE_FILE, JSON.stringify(updated, null, 2) + "\n", "utf8"); console.log( - `Done. ${payloads} payload(s), ${vpArtifacts} VP artifact(s), ${oid4vpRequests} OID4VP request(s).`, + `Done. ${payloads} payload(s), ${resultArtifacts} result artifact(s), ${oid4vpRequests} OID4VP request(s).`, ); } diff --git a/spec/SPEC_SOURCE.json b/spec/SPEC_SOURCE.json index 24de614..15e1016 100644 --- a/spec/SPEC_SOURCE.json +++ b/spec/SPEC_SOURCE.json @@ -1,9 +1,9 @@ { "repo": "proof/x401", - "ref": "4057cacc41e9e20547f6a6949946e9e1115b37d2", + "ref": "3f73cf912cd05a40bf8de3fe6c1f4bb720341336", "branch": "main", "version": "0.2.0", "spec_url": "https://x401.proof.com/spec", "schema_url": "https://x401.id/spec/schemas/request.json", - "fetched_at": "2026-06-24T08:55:14.795Z" + "fetched_at": "2026-06-28T22:18:28.884Z" } diff --git a/spec/conformance.md b/spec/conformance.md index 0502975..a370d9b 100644 --- a/spec/conformance.md +++ b/spec/conformance.md @@ -1,70 +1,67 @@ # x401 conformance map -Maps the spec's RFC 2119 normative statements to where this library enforces them — or -records why they are out of scope. The goal is to make "did we miss a MUST?" answerable. +Maps the spec's RFC 2119 normative statements to where this library enforces them, or records why +they are out of scope. The goal is to make "did we miss a MUST?" answerable. -- **Source of truth:** `spec/spec.md` at the ref in `spec/SPEC_SOURCE.json` (currently - x401 **0.2.0**, proof/x401 `main` — PR #15 merged). -- **Drift detection:** `spec/normative-ledger.json` snapshots every normative statement. - After `node scripts/sync-spec-fixtures.ts`, run `node scripts/extract-normative.ts` to see - what was **added/removed** since the ledger. Triage new statements here, then +- **Source of truth:** `spec/spec.md` at the ref in `spec/SPEC_SOURCE.json` (currently x401 + **0.2.0**, proof/x401 `main`). +- **Drift detection:** `spec/normative-ledger.json` snapshots every normative statement. After + `node scripts/sync-spec-fixtures.ts`, run `node scripts/extract-normative.ts` to see what was + added or removed since the ledger. Triage new statements here, then run `node scripts/extract-normative.ts --update`. -- **Scope of this library:** it produces, encodes, decodes, and structurally validates the - x401 **wire objects**. It does **not** verify credentials, validate presentation bindings, - sign/compose the OpenID4VP request, perform the DC API call, or make HTTP/transport - decisions. Those statements are marked out of scope with the responsible layer. +- **Scope of this library:** it produces, encodes, decodes, and structurally validates the x401 wire + objects. It does not verify credentials, validate result bindings, sign or compose the OpenID4VP + request, perform the Credential Manager call, or make HTTP transport decisions. Those statements + are marked out of scope with the responsible layer. -## In scope — enforced or produced by this library +## In scope: enforced or produced by this library -| Spec requirement | Where | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `scheme` MUST be `"x401"` (payload, error, token) | `validate.ts` `parseX401Payload` / `parseX401ErrorObject` / `parseX401TokenObject`; builders in `verifier.ts`/`agent.ts` set the constant. Tests: `x401.test.ts`, `spec-fixtures.test.ts` | -| `version` REQUIRED | `validate.ts` (all three parsers); `X401_VERSION` constant | -| `presentation_requirements` REQUIRED; `requests` a non-empty array; each `protocol` is `openid4vp-v1-signed`/`openid4vp-v1-unsigned`; each `data` an object | `validate.ts` `parseX401Payload`, `verifier.ts` `buildPayload`. Tests: `spec-schema.test.ts` (Appendix C schema), `x401.test.ts` | -| `oauth` REQUIRED; `oauth.token_endpoint` REQUIRED | `validate.ts` `parseX401Payload`. Tests: `spec-schema.test.ts`, `x401.test.ts` | -| Payload encoded value MUST be base64url UTF-8 JSON (RFC 4648 §5, no padding); decoded MUST be a single JSON object | `encoding.ts` (`@owf/identity-common` base64url) | -| MUST NOT combine multiple objects in one proof header via commas/lists; comma value MUST be treated as invalid | `encoding.ts` `decodeProofHeader` comma guard. Test: `x401.test.ts` | -| VP Artifact MUST contain exactly one of `response` / `presentation_uri` | `validate.ts` `parseVPArtifact`. Tests: `x401.test.ts` (both/neither), `spec-fixtures.test.ts` | -| `response` is the `{ protocol, data }` DC API result | `validate.ts` `parseVPArtifact`; `agent.ts` `buildVPArtifact` | -| `presentation_uri` MUST be an `https` URL | `validate.ts` `parseVPArtifact`. Test: `x401.test.ts` (non-https rejected) | -| Token Object `token_type` MUST be `"Bearer"`; `access_token` REQUIRED | `validate.ts` `parseX401TokenObject`; `agent.ts` `buildTokenObject` | -| Error Object `error` REQUIRED | `validate.ts` `parseX401ErrorObject` | -| Token-exchange fixed params (`grant_type`, `subject_token_type`, Bearer) MUST NOT be repeated in the payload | not present in the payload type; set only on the form by `agent.ts` `buildTokenExchangeForm`; verified by `verifier.ts` `parseTokenExchange`. Test: `x401.test.ts` | -| Embedded ``: tag `data`, `value="application/json;x401=proof-required"`, `hidden`, single JSON object that is a valid payload and MUST include a `$schema` member = `https://x401.id/spec/schemas/request.json` | `verifier.ts` `embedHtmlData`; `agent.ts` `detectProofRequirement` + `parseX401Payload`. Test: `x401.test.ts` (embedded round-trip) | -| Embedded object subject to the same structural validation as a header payload | `agent.ts` `detectProofRequirement` runs `parseX401Payload`. Test: `x401.test.ts` | -| Agent MUST NOT modify any entry in `presentation_requirements` | `agent.ts` `getDigitalCredentialRequest` returns it unmodified; library never mutates it | -| A relaying intermediary MUST add a `return_uri` member (an `https` URL) to the forwarded payload; the Verifier never sets it | `agent.ts` `addReturnUri` (https-validated; `buildPayload` never emits it); `validate.ts` `parseX401Payload` enforces https. Tests: `x401.test.ts` | +| Spec requirement | Where | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scheme` MUST be `"x401"` (payload, error, token) | `validate.ts` `parseX401Payload` / `parseX401ErrorObject` / `parseX401TokenObject`; builders in `verifier.ts`/`agent.ts` set the constant. Tests: `x401.test.ts`, `spec-fixtures.test.ts` | +| `version` REQUIRED | `validate.ts` (all three parsers); `X401_VERSION` constant | +| `credential_requirements` REQUIRED; `digital.requests` a non-empty array; each `protocol` is `openid4vp-v1-signed` or `openid4vp-v1-unsigned`; each `data` an object | `validate.ts` `parseX401Payload`, `verifier.ts` `buildPayload`. Tests: `spec-schema.test.ts`, `x401.test.ts` | +| `oauth` REQUIRED; `oauth.token_endpoint` REQUIRED | `validate.ts` `parseX401Payload`. Tests: `spec-schema.test.ts`, `x401.test.ts` | +| Payload encoded value MUST be base64url UTF-8 JSON (RFC 4648 section 5, no padding); decoded value MUST be a single JSON object | `encoding.ts` (`@owf/identity-common` base64url) | +| MUST NOT combine multiple objects in one proof header through commas or lists; comma value MUST be treated as invalid | `encoding.ts` `decodeProofHeader` comma guard. Test: `x401.test.ts` | +| Result Artifact MUST contain exactly one of `credential_result` or `credential_result_uri` | `validate.ts` `parseResultArtifact`. Tests: `x401.test.ts`, `spec-fixtures.test.ts` | +| `credential_result` is the `{ protocol, data }` Credential Manager result | `validate.ts` `parseResultArtifact`; `agent.ts` `buildResultArtifact` | +| `credential_result_uri` MUST be an `https` URL | `validate.ts` `parseResultArtifact`. Test: `x401.test.ts` | +| Token Object `token_type` MUST be `"Bearer"`; `access_token` REQUIRED | `validate.ts` `parseX401TokenObject`; `agent.ts` `buildTokenObject` | +| Error Object `error` REQUIRED | `validate.ts` `parseX401ErrorObject` | +| Token-exchange fixed params (`grant_type`, `subject_token_type`, Bearer) MUST NOT be repeated in the payload | not present in the payload type; set only on the form by `agent.ts` `buildTokenExchangeForm`; verified by `verifier.ts` `parseTokenExchange`. Test: `x401.test.ts` | +| Embedded ``: tag `data`, `value="application/json;x401=proof-required"`, `hidden`, single JSON object that is a valid payload and SHOULD include a `$schema` member = `https://x401.id/spec/schemas/request.json` | `verifier.ts` `embedHtmlData`; `agent.ts` `detectProofRequirement` plus `parseX401Payload`. Test: `x401.test.ts` | +| Embedded object subject to the same structural validation as a header payload | `agent.ts` `detectProofRequirement` runs `parseX401Payload`. Test: `x401.test.ts` | +| Agent MUST NOT modify any entry in `credential_requirements` | `agent.ts` `getCredentialRequestOptions` returns it unmodified; library never mutates it | +| A relaying intermediary MUST add a `return_uri` member (an `https` URL) to the forwarded payload; the Verifier never sets it | `agent.ts` `addReturnUri` (https-validated); `buildPayload` never emits it; `validate.ts` `parseX401Payload` enforces https. Tests: `x401.test.ts` | -## Out of scope — responsibility of another layer +## Out of scope: responsibility of another layer -These normative statements are real but fall outside an encode/decode/validate library. +These normative statements are real but fall outside an encode, decode, and validate library. -- **Remote handler processing** (matching `requests[]` against held credentials, satisfying - `dcql_query` incl. `credential_sets`/`claim_sets`, Holder selection, producing the presentation, - POSTing it to `return_uri`): the remote wallet/handler. This SDK only adds and validates the - `return_uri` member; it does not act as a handler. -- **Verifier proof validation & crypto** (the "The Verifier MUST:" list, Verifier Binding, - nonce freshness/replay, dereferencing a `presentation_uri`, unique-URI issuance, issuer - trust enforcement, `trusted_authorities`): the verifier application. This library does not - verify presentations or sign requests (`CLAUDE.md` Hard Rules 1–3). +- **Remote handler processing** (matching `credential_requirements.digital.requests[]` against held + credentials, satisfying `dcql_query` including `credential_sets` and `claim_sets`, holder + selection, producing the credential result, posting it to `return_uri`): the remote wallet or + handler. This SDK only adds and validates the `return_uri` member; it does not act as a handler. +- **Verifier proof validation and crypto** (Verifier Binding, nonce freshness and replay, + dereferencing a `credential_result_uri`, unique URI issuance, issuer trust enforcement, + `trusted_authorities`): the verifier application. This library does not verify credential results + or sign requests. - **Credential verification** (issuer trust, status, revocation, claim satisfaction): - `@proof.com/proof-vc-common`. `vp_token`/`response.data` is opaque here. -- **Agent runtime / transport** (obtaining a presentation via `navigator.credentials.get`, - relaying, remote fulfillment, retrying the route): the Agent application. -- **OpenID4VP request composition/signing** (the JAR, `client_id`, `expected_origins`, - `nonce`, `dcql_query`, `exp`): the verifier; carried opaque in `data`. This includes the - "Composing a Request for Both Native and Relayed Fulfillment" rules added when PR #15 merged - (self-contained request, `x5c`/resolvable `client_id`, carrying all inputs inside the request - object, transport members optional) — all verifier request-authoring concerns, not wire-object - structure. + `@proof.com/proof-vc-common`. `vp_token` and `credential_result.data` are opaque here. +- **Agent runtime and transport** (obtaining a credential result through + `navigator.credentials.get`, relaying, remote fulfillment, retrying the route): the Agent + application. +- **OpenID4VP request composition and signing** (the JAR, `client_id`, `expected_origins`, `nonce`, + `dcql_query`, `exp`): the Verifier; carried opaque in `data`. - **HTTP semantics** (status-code independence, `WWW-Authenticate` non-use, `402` payment - separation, `Cache-Control`/`Vary`, CORS exposure): the HTTP server/deployment. -- **Verification Token issuance, scope, binding, holder identity**; **Agent binding** - (OPTIONAL): the verifier/deployment. + separation, `Cache-Control`/`Vary`, CORS exposure): the HTTP server or deployment. +- **Verification Token issuance, scope, binding, holder identity**, and **Agent binding**: the + verifier or deployment. ## Known coverage gap -Only the **PROOF-REQUIRED payload** has an official JSON Schema (Appendix C). The VP Artifact, -Error Object, and Token Object are checked against extracted spec examples + these parsers, not a -published schema. If the spec later publishes schemas for those objects, add them to -`spec/fixtures/` via `sync-spec-fixtures.ts` and extend `spec-schema.test.ts`. +Only the `PROOF-REQUEST` payload has an official JSON Schema. The Result Artifact, Error Object, and +Token Object are checked against extracted spec examples plus these parsers, not a published schema. +If the spec later publishes schemas for those objects, add them to `spec/fixtures/` through +`sync-spec-fixtures.ts` and extend `spec-schema.test.ts`. diff --git a/spec/fixtures/error-object.json b/spec/fixtures/error-object.json index ffe5864..a4979b2 100644 --- a/spec/fixtures/error-object.json +++ b/spec/fixtures/error-object.json @@ -1,7 +1,7 @@ { "scheme": "x401", "version": "0.2.0", - "error": "invalid_presentation", - "error_description": "The presentation did not satisfy the route proof requirement.", + "error": "invalid_result", + "error_description": "The credential result did not satisfy the route proof requirement.", "request_id": "proof-template-financial-customer-v1" } diff --git a/spec/fixtures/payload-1.json b/spec/fixtures/payload-1.json index 042b3c6..a80b6b4 100644 --- a/spec/fixtures/payload-1.json +++ b/spec/fixtures/payload-1.json @@ -1,9 +1,8 @@ { "scheme": "x401", "version": "0.2.0", - "presentation_requirements": {}, + "credential_requirements": {}, "oauth": {}, - "trust_establishment": "https://...", "request_id": "...", "satisfied_requirements": [], "payment": {} diff --git a/spec/fixtures/payload-2.json b/spec/fixtures/payload-2.json index 6e485a9..43e36d6 100644 --- a/spec/fixtures/payload-2.json +++ b/spec/fixtures/payload-2.json @@ -1,15 +1,17 @@ { "scheme": "x401", "version": "0.2.0", - "presentation_requirements": { - "requests": [ - { - "protocol": "openid4vp-v1-signed", - "data": { - "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + "credential_requirements": { + "digital": { + "requests": [ + { + "protocol": "openid4vp-v1-signed", + "data": { + "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + } } - } - ] + ] + } }, "oauth": { "token_endpoint": "https://bank.example.com/oauth/token" diff --git a/spec/fixtures/payload-3.json b/spec/fixtures/payload-3.json index 058f99c..e72b91b 100644 --- a/spec/fixtures/payload-3.json +++ b/spec/fixtures/payload-3.json @@ -1,15 +1,17 @@ { "scheme": "x401", "version": "0.2.0", - "presentation_requirements": { - "requests": [ - { - "protocol": "openid4vp-v1-signed", - "data": { - "request": "eyJ..." + "credential_requirements": { + "digital": { + "requests": [ + { + "protocol": "openid4vp-v1-signed", + "data": { + "request": "eyJ..." + } } - } - ] + ] + } }, "oauth": { "token_endpoint": "https://bank.example.com/oauth/token" diff --git a/spec/fixtures/payload-4.json b/spec/fixtures/payload-4.json index f8caafd..43e36d6 100644 --- a/spec/fixtures/payload-4.json +++ b/spec/fixtures/payload-4.json @@ -1,20 +1,21 @@ { "scheme": "x401", "version": "0.2.0", - "presentation_requirements": { - "requests": [ - { - "protocol": "openid4vp-v1-signed", - "data": { - "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + "credential_requirements": { + "digital": { + "requests": [ + { + "protocol": "openid4vp-v1-signed", + "data": { + "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + } } - } - ] + ] + } }, "oauth": { "token_endpoint": "https://bank.example.com/oauth/token" }, - "trust_establishment": "https://bank.example.com/.well-known/x401/trust/financial-customer-v1", "request_id": "proof-template-financial-customer-v1", "satisfied_requirements": [ "urn:example:x401:satisfaction:financial-customer:v1" diff --git a/spec/fixtures/payload-5.json b/spec/fixtures/payload-5.json index 525c7b4..e8cf614 100644 --- a/spec/fixtures/payload-5.json +++ b/spec/fixtures/payload-5.json @@ -1,15 +1,17 @@ { "scheme": "x401", "version": "0.2.0", - "presentation_requirements": { - "requests": [ - { - "protocol": "openid4vp-v1-signed", - "data": { - "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + "credential_requirements": { + "digital": { + "requests": [ + { + "protocol": "openid4vp-v1-signed", + "data": { + "request": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..." + } } - } - ] + ] + } }, "oauth": { "token_endpoint": "https://bank.example.com/oauth/token" diff --git a/spec/fixtures/request.schema.json b/spec/fixtures/request.schema.json index 9af2e9b..f5a9823 100644 --- a/spec/fixtures/request.schema.json +++ b/spec/fixtures/request.schema.json @@ -7,7 +7,7 @@ "required": [ "scheme", "version", - "presentation_requirements", + "credential_requirements", "oauth" ], "properties": { @@ -25,39 +25,48 @@ "type": "string", "description": "The x401 payload version." }, - "presentation_requirements": { + "credential_requirements": { "type": "object", "required": [ - "requests" + "digital" ], - "description": "The composed Digital Credentials request (a DigitalCredentialRequestOptions value), usable as the digital member of navigator.credentials.get().", + "description": "The Verifier-composed credential request, a CredentialRequestOptions value usable as the argument to navigator.credentials.get(). This version of x401 specifies the digital member; additional navigator.credentials.get() request types (e.g. publicKey) may be specified in future versions.", "properties": { - "requests": { - "type": "array", - "minItems": 1, - "description": "Digital Credentials request entries. Every entry MUST be a valid OpenID4VP request for the DC API.", - "items": { - "type": "object", - "required": [ - "protocol", - "data" - ], - "properties": { - "protocol": { - "type": "string", - "enum": [ - "openid4vp-v1-signed", - "openid4vp-v1-unsigned" - ], - "description": "The DC API protocol identifier. openid4vp-v1-signed is RECOMMENDED; openid4vp-v1-unsigned is permitted. See Verifier Binding." - }, - "data": { + "digital": { + "type": "object", + "required": [ + "requests" + ], + "description": "The composed Digital Credentials request, a DigitalCredentialRequestOptions value (the value the digital member of navigator.credentials.get() takes).", + "properties": { + "requests": { + "type": "array", + "minItems": 1, + "description": "Digital Credentials request entries. Every entry MUST be a valid OpenID4VP request for the DC API.", + "items": { "type": "object", - "description": "Protocol-specific request data. For openid4vp-v1-signed, an object carrying the signed OpenID4VP request (e.g. { \"request\": \"\" }); for openid4vp-v1-unsigned, the OpenID4VP request parameters directly.", + "required": [ + "protocol", + "data" + ], "properties": { - "request": { + "protocol": { "type": "string", - "description": "The signed OpenID4VP request (a JWT-Secured Authorization Request); present for openid4vp-v1-signed." + "enum": [ + "openid4vp-v1-signed", + "openid4vp-v1-unsigned" + ], + "description": "The DC API protocol identifier. openid4vp-v1-signed is RECOMMENDED; openid4vp-v1-unsigned is permitted. See Verifier Binding." + }, + "data": { + "type": "object", + "description": "Protocol-specific request data. For openid4vp-v1-signed, an object carrying the signed OpenID4VP request (e.g. { \"request\": \"\" }); for openid4vp-v1-unsigned, the OpenID4VP request parameters directly.", + "properties": { + "request": { + "type": "string", + "description": "The signed OpenID4VP request (a JWT-Secured Authorization Request); present for openid4vp-v1-signed." + } + } } } } @@ -75,7 +84,7 @@ "token_endpoint": { "type": "string", "format": "uri", - "description": "OAuth 2.0 token endpoint where the Agent can exchange a VP Artifact for a Verification Token." + "description": "OAuth 2.0 token endpoint where the Agent can exchange a Result Artifact for a Verification Token." }, "audience": { "type": "string", @@ -89,11 +98,6 @@ }, "additionalProperties": false }, - "trust_establishment": { - "type": "string", - "format": "uri", - "description": "Optional acquisition and discovery hint: HTTPS URL for a DIF Credential Trust Establishment document. Issuer enforcement is governed by the signed request (DCQL trusted_authorities), not by this member." - }, "request_id": { "type": "string", "description": "Optional Agent-visible hint: a stable verifier-defined identifier for the proof template." @@ -108,7 +112,7 @@ "return_uri": { "type": "string", "format": "uri", - "description": "Optional. Added by a relaying intermediary (never by the Verifier) to tell a remote handler where to POST the presentation result. See Relayed Delivery to a Remote Handler." + "description": "Optional. Added by a relaying intermediary (never by the Verifier) to tell a remote handler where to POST the credential result. See Relayed Delivery to a Remote Handler." }, "payment": { "type": "object", diff --git a/spec/fixtures/vp-artifact-1.json b/spec/fixtures/result-artifact-1.json similarity index 57% rename from spec/fixtures/vp-artifact-1.json rename to spec/fixtures/result-artifact-1.json index c1e7aed..adb2d1c 100644 --- a/spec/fixtures/vp-artifact-1.json +++ b/spec/fixtures/result-artifact-1.json @@ -1,7 +1,7 @@ { "request_id": "proof-template-financial-customer-v1", - "response": { + "credential_result": { "protocol": "openid4vp-v1-signed", - "data": "" + "data": "" } } diff --git a/spec/fixtures/vp-artifact-2.json b/spec/fixtures/result-artifact-2.json similarity index 57% rename from spec/fixtures/vp-artifact-2.json rename to spec/fixtures/result-artifact-2.json index c1e7aed..adb2d1c 100644 --- a/spec/fixtures/vp-artifact-2.json +++ b/spec/fixtures/result-artifact-2.json @@ -1,7 +1,7 @@ { "request_id": "proof-template-financial-customer-v1", - "response": { + "credential_result": { "protocol": "openid4vp-v1-signed", - "data": "" + "data": "" } } diff --git a/spec/fixtures/vp-artifact-5.json b/spec/fixtures/result-artifact-3.json similarity index 52% rename from spec/fixtures/vp-artifact-5.json rename to spec/fixtures/result-artifact-3.json index 2e6855e..178006c 100644 --- a/spec/fixtures/vp-artifact-5.json +++ b/spec/fixtures/result-artifact-3.json @@ -1,5 +1,5 @@ { "request_id": "proof-template-financial-customer-v1", - "presentation_uri": "https://bank.example.com/.well-known/x401/presentations/abc123", + "credential_result_uri": "https://bank.example.com/.well-known/x401/results/abc123", "expires_at": "2026-05-06T18:50:00Z" } diff --git a/spec/fixtures/vp-artifact-4.json b/spec/fixtures/result-artifact-4.json similarity index 57% rename from spec/fixtures/vp-artifact-4.json rename to spec/fixtures/result-artifact-4.json index c1e7aed..adb2d1c 100644 --- a/spec/fixtures/vp-artifact-4.json +++ b/spec/fixtures/result-artifact-4.json @@ -1,7 +1,7 @@ { "request_id": "proof-template-financial-customer-v1", - "response": { + "credential_result": { "protocol": "openid4vp-v1-signed", - "data": "" + "data": "" } } diff --git a/spec/fixtures/vp-artifact-3.json b/spec/fixtures/result-artifact-5.json similarity index 52% rename from spec/fixtures/vp-artifact-3.json rename to spec/fixtures/result-artifact-5.json index 2e6855e..178006c 100644 --- a/spec/fixtures/vp-artifact-3.json +++ b/spec/fixtures/result-artifact-5.json @@ -1,5 +1,5 @@ { "request_id": "proof-template-financial-customer-v1", - "presentation_uri": "https://bank.example.com/.well-known/x401/presentations/abc123", + "credential_result_uri": "https://bank.example.com/.well-known/x401/results/abc123", "expires_at": "2026-05-06T18:50:00Z" } diff --git a/spec/normative-ledger.json b/spec/normative-ledger.json index 5f9a76b..98da9d3 100644 --- a/spec/normative-ledger.json +++ b/spec/normative-ledger.json @@ -1,89 +1,90 @@ [ "1. MUST consider the `requests[]` entries whose `protocol` and credential formats it implements, and among those, MUST attempt to satisfy each entry's `dcql_query` — including any `credential_sets` / `claim_sets` alternatives within it — against the credentials available to it, with Holder selection where applicable. Which entry is used is the *outcome* of this matching, not a prior choice; an entry is usable only if a held credential satisfies its query.", - "1. MUST include `PROOF-REQUIRED: ` when proof is required or advertised.", + "1. MUST include `PROOF-REQUEST: ` when proof is required or advertised.", "1. MUST make each entry a valid OpenID4VP request for the Digital Credentials API, using `protocol: \"openid4vp-v1-signed\"` (RECOMMENDED) or `protocol: \"openid4vp-v1-unsigned\"`.", "1. MUST treat the parsed JSON object as an x401 payload subject to the same structural validation and composed-request processing defined elsewhere in this specification.", "1. MUST treat the response as a proof requirement.", "1. MUST use the tag name `data`.", - "1. The Agent MUST NOT modify any entry in `presentation_requirements`. The request signature binds its contents, so any modification invalidates it.", - "1. The `presentation_uri` MUST be an `https` URL.", + "1. The Agent MUST NOT modify any entry in `credential_requirements`. The request signature binds its contents, so any modification invalidates it.", + "1. The `credential_result_uri` MUST be an `https` URL.", "1. when the deployment binds the Agent, MUST be issued to the [[ref: Agent Identifier]] bound during the retry, and MUST NOT rely on the credential subject as the token holder identity unless the credential subject is also the Agent;", - "10. MUST evaluate issuer trust, status, revocation, and policy constraints independently of any Agent-side interpretation of the Issuer Trust List.", + "10. MUST evaluate issuer trust, status, revocation, and policy constraints independently of any Agent-side interpretation of the request's `trusted_authorities`.", "10. MUST retry the same route that produced the x401 proof requirement with one of:", "11. MUST NOT replace an existing application `Authorization` credential with an x401 Verification Token unless the deployment explicitly defines the returned token as valid for that route's ordinary authorization processing.", - "11. MUST accept a VP Artifact in a `PROOF-PRESENTATION` request header for protected-route retry, in both its inline and by-reference forms.", - "12. MUST treat a `PROOF-RESPONSE` carrying an x401 Error Object as an x401 proof failure for the route-scoped proof attempt, regardless of the HTTP status code.", - "13. MUST validate Verification Tokens on protected-route retry according to token scope, audience, expiration, any Agent binding, and satisfied requirement metadata, whether the token arrives in `Authorization` or as an x401 Token Object in `PROOF-PRESENTATION`.", - "14. MUST bind any Verification Token carried in `PROOF-PRESENTATION` to the existing application caller, credential, client, key, or Agent Identifier required by the protected route when an `Authorization` header is also present.", + "11. MUST accept a Result Artifact in a `PROOF-RESPONSE` request header for protected-route retry, in both its inline and by-reference forms.", + "12. MUST treat a `PROOF-RESULT` carrying an x401 Error Object as an x401 proof failure for the route-scoped proof attempt, regardless of the HTTP status code.", + "13. MUST validate Verification Tokens on protected-route retry according to token scope, audience, expiration, any Agent binding, and satisfied requirement metadata, whether the token arrives in `Authorization` or as an x401 Token Object in `PROOF-RESPONSE`.", + "14. MUST bind any Verification Token carried in `PROOF-RESPONSE` to the existing application caller, credential, client, key, or Agent Identifier required by the protected route when an `Authorization` header is also present.", "16. MUST use `402 Payment Required` separately if payment is required and remains unsatisfied.", - "2. For a satisfiable entry, MUST read the OpenID4VP request from its `data` — the claims of the signed request object for `openid4vp-v1-signed`, or the request parameters directly for `openid4vp-v1-unsigned` — and produce a presentation that satisfies that `dcql_query`, bound to its `nonce`. It honors `client_metadata` for accepted formats and any response-encryption key, and, for a signed request, binds the presentation's audience to the request's `client_id`.", + "2. For a satisfiable entry, MUST read the OpenID4VP request from its `data` — the claims of the signed request object for `openid4vp-v1-signed`, or the request parameters directly for `openid4vp-v1-unsigned` — and produce a result that satisfies that `dcql_query`, bound to its `nonce`. It honors `client_metadata` for accepted formats and any response-encryption key, and, for a signed request, binds the result's audience to the request's `client_id`.", "2. MUST be scoped to the Verifier audience and to the route, policy, action, resource, or resource class for which proof was accepted;", - "2. MUST extract the `PROOF-REQUIRED` field value and base64url-decode it as a UTF-8 JSON [[ref: x401 Payload]].", - "2. MUST include a valid base64url-encoded x401 payload in `PROOF-REQUIRED`.", + "2. MUST extract the `PROOF-REQUEST` field value and base64url-decode it as a UTF-8 JSON [[ref: x401 Payload]].", + "2. MUST include a valid base64url-encoded x401 payload in `PROOF-REQUEST`.", "2. MUST make its identity and request-signing key resolvable from the request alone. The Verifier SHOULD embed its signing certificate chain in the request JWS `x5c` header, or use a `client_id` whose key material is publicly resolvable (for example a `did:` or `https:` scheme), so a handler with no prior relationship can verify the signature and confirm it matches `client_id` offline. A Verifier MUST NOT rely on a `client_id` scheme or key whose resolution depends on the invoking Web origin or on prior DC-API interaction, because neither exists on the relayed path.", "2. MUST set the `value` attribute to the MIME-type expression `application/json;x401=proof-required`. The `x401` parameter identifies the embedded carrier and signals the role of the element's text content.", - "2. The Verifier MUST issue a unique `presentation_uri` for each presentation and MUST NOT reuse a URI value across presentations. Uniqueness per presentation is what lets the Verifier treat the reference as single-use and bind it to one retry.", + "2. The Verifier MUST issue a unique `credential_result_uri` for each result and MUST NOT reuse a URI value across results. Uniqueness per result is what lets the Verifier treat the reference as single-use and bind it to one retry.", "3. MUST carry every input a fulfiller needs inside the request object: the `nonce`, the `dcql_query` (including any `credential_sets` / `claim_sets` alternatives), the accepted formats and any response-encryption key in `client_metadata`, and the `exp`. A remote handler reads these directly from the request `data` — the signed request object's claims for a signed request — not from any API surface, so a value referenced only through the DC-API or a same-origin session is unavailable to it.", "3. MUST expire, and SHOULD be short-lived;", "3. MUST set the `hidden` attribute so the element is not visually rendered.", "3. MUST treat the Digital Credentials API transport members as not applicable to relayed delivery: it does not return through `response_mode: dc_api`/`dc_api.jwt` (it returns to `return_uri` instead) and does not enforce `expected_origins` (there is no invoking Web origin).", "3. MUST use an HTTP status code appropriate for the overall response and MUST NOT rely on the status code alone to convey x401 proof state.", "3. MUST validate the decoded payload structure and process the `proof` object.", - "3. The Agent MUST arrange to acquire the [[ref: Presentation Result]] returned for the request, whether it invokes the request itself, relays it, or acquires a remotely generated result.", - "4. MUST NOT weaken or alter the `dcql_query` or `nonce`, and MUST deliver the resulting [[ref: Presentation Result]] to `return_uri`. If it can satisfy no entry, it returns no presentation and SHOULD signal that failure to the intermediary rather than returning a partial or substitute result.", + "3. The Agent MUST arrange to acquire the [[ref: Credential Result]] returned for the request, whether it invokes the request itself, relays it, or acquires a remotely generated result.", + "4. MUST NOT weaken or alter the `dcql_query` or `nonce`, and MUST deliver the resulting [[ref: Credential Result]] to `return_uri`. If it can satisfy no entry, it returns no result and SHOULD signal that failure to the intermediary rather than returning a partial or substitute result.", "4. MUST contain a single JSON object as its text content. The JSON object MUST be a valid x401 payload as defined in [x401 Payload](#x401-payload), and MUST include a `$schema` member whose value is the JSON Schema URL for the x401 request object, `https://x401.id/spec/schemas/request.json`. The `$schema` member is an informational marker that allows AI scrapers, content processors, and validators that retain only the JSON object to recognize it as an x401 proof requirement without prior knowledge of the surrounding HTML carrier.", - "4. MUST include a `presentation_requirements` whose entries are valid OpenID4VP requests for the DC API, using `openid4vp-v1-signed` (RECOMMENDED) or `openid4vp-v1-unsigned`.", - "4. MUST treat `presentation_requirements` as the Verifier-composed [[ref: Digital Credentials Request]] and MUST NOT modify any of its entries.", + "4. MUST include a `credential_requirements` whose `digital` member's entries are valid OpenID4VP requests for the DC API, using `openid4vp-v1-signed` (RECOMMENDED) or `openid4vp-v1-unsigned`.", + "4. MUST treat `credential_requirements` as the Verifier-composed credential request and MUST NOT modify any of its entries.", "4. SHOULD still set the Digital Credentials API transport members — `response_mode` (`dc_api` / `dc_api.jwt`) and `expected_origins` — for the native path, but MUST NOT make correct processing depend on them. A remote handler treats them as not applicable (it returns to `return_uri` and there is no invoking origin to pre-authorize), so a request whose only Verifier binding is `expected_origins` degrades to `nonce`-only when relayed.", - "5. MUST obtain a [[ref: Presentation Result]] for `presentation_requirements` by invoking it through a native credential method, relaying it to a Wallet or remote service, or acquiring a remotely generated result.", + "5. MUST obtain a [[ref: Credential Result]] for `credential_requirements` by invoking it through a native credential method, relaying it to a Credential Manager or remote service, or acquiring a remotely generated result.", "5. SHOULD use signed requests and set their `client_id` and `expected_origins`; when using unsigned requests, MUST account for the weaker binding described in [Verifier Binding](#verifier-binding).", "6. MUST include OAuth token exchange metadata in `oauth`.", - "8. MUST NOT enumerate verifier-approved issuers inline in the x401 payload.", - "8. MUST NOT treat any Agent-side interpretation of the Issuer Trust List as proof of verifier acceptance.", - "9. MUST package the presentation result as a VP Artifact, inline or as a [[ref: Presentation Reference]].", - "9. MUST validate presentations according to the proof validation rules in this specification and the credential format rules it relies upon, dereferencing a [[ref: Presentation Reference]] when one is supplied.", - "A VP Artifact MUST contain exactly one of `response` or `presentation_uri`.", - "A Verifier MAY use the validated signing key, key directory authority, or derived service identity as the Agent Identifier, or as evidence that maps to an Agent Identifier. The Verifier MUST still validate the Wallet presentation binding for the request mode, the credential query satisfaction, issuer trust, token scope, and payment boundary. Web Bot Auth identifies the calling automation or service; it does not by itself prove the credential subject, satisfy the credential query, or prove end-user delegation.", - "A Verifier that emits embedded `` elements MUST still enforce proof on the protected resource through the normal `PROOF-REQUIRED` / `PROOF-PRESENTATION` exchange. Embedding a requirement in HTML is informational disclosure and does not by itself grant access.", - "A Verifier that may have its request fulfilled either natively — invoked through `navigator.credentials.get()` by a Wallet enforcing the Digital Credentials API — or by a remote handler that parses the request and generates a presentation manually MUST compose the request so it is self-contained and verifiable by a party with no prior relationship and no browser context. The governing test is: *could a Wallet that has never interacted with this Verifier verify the request and produce a correctly bound presentation from the request bytes alone?* A request composed only for the native path can silently fail this test even though a DC-API Wallet accepts it. To satisfy both paths, a Verifier:", - "A `` element placed at the document level applies to the page as a whole and SHOULD be used as a body-side mirror of the route-scoped `PROOF-REQUIRED` header so that header-blind clients can still discover the requirement. A response MAY include multiple `