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>), read the Verifier-composedpresentation_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-composedpresentation_requirements), emit the embedded<data>mirror, decode incoming VP Artifacts / Token Objects, parse token-exchange requests, encode error objects.
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.
- Minimize runtime dependencies. The only permitted runtime dependency is
@owf/identity-common(encoding helpers), which proof-vc-common also uses. Otherwise rely on the WebCrypto-era globals. Do not add credential, crypto, or HTTP-framework dependencies. - Never verify credentials here.
vp_tokenis opaque. SD-JWT-VC verification, issuer trust, and claim checks belong to the credential library (@proof.com/proof-vc-common). Do not add@sd-jwt/*,jose, X.509, or DCQL evaluation logic. - Building the OpenID4VP request / wallet transport stays out. That is the Agent's job.
- Prompt before publishing. Never bump version, push tags, create a Release, or trigger the publish workflow without explicit confirmation. Publishes are permanent.
- Run
yarn check-allandyarn testbefore any commit or push. - Keep
yarn publinton--pack npm. - Keep
engines.nodeat>=22.0.0and keep the CItest-matrixcovering it. This is the consumer runtime floor; consumers run the compileddist, which uses only long-stable globals (URL/URLSearchParams) and runs on any maintained LTS. Dev and CI use Node 24 (.node-version, active LTS). Thetest-matrixjob runsyarn teston Node 22 and 24; the 22 leg resolves to the latest 22.x because the native.tstest runner needs default type stripping (Node >=22.18), so never pin the matrix low leg below that. Don't raise the consumer floor to match the dev pin. - Never use
eslint-disable,@ts-ignore, or@ts-expect-erroras a workaround. Fix the underlying code or surface the rule to the user for a config decision.
verbatimModuleSyntax: true— useimport type/export type.noUncheckedIndexedAccess: true— indexing returnsT | undefined; use!only when access is provably safe (e.g. after a length check).exactOptionalPropertyTypes: true— set optional fields with conditional spread:...(value !== undefined && { value }).- Local imports use the
.tsextension (rewriteRelativeImportExtensionsrewrites to.js). - Wire-level types use snake_case to match the JSON wire format (
token_endpoint,vp_token).
| Command | Purpose |
|---|---|
yarn check-all |
Full check: format, lint, typecheck, test, publint |
yarn build |
tsc emit to dist/ |
yarn test |
node --test tests/*.test.ts |
yarn typecheck |
tsc --noEmit |
yarn lint |
eslint --fix |
yarn format |
prettier --write |
yarn publint |
publint --pack npm (keep the flag) |
- Yarn is pinned via
packageManager: yarn@4.17.0(.yarn/releases/yarn-4.17.0.cjs). Runcorepack enableso the project yarn is used; CI does the same. .yarnrc.ymlconfig:nodeLinker: node-modules, immutable installs (enableImmutableInstalls: true- no--frozen-lockfileneeded),enableScripts: false(no postinstall scripts - a dep needing a build step at install won't run it),npmMinimalAgeGate: 1w(deps published <1 week ago won't install; matches the dependabot 7-day cooldown).yarn.lockis the only lockfile.
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): flatX401Payload,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/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/verifiernamespaces).
Prompt before publishing (Hard Rule 2).
- Auth: npm Trusted Publishing via OIDC (no
NPM_TOKEN). - Trigger: GitHub Release published →
.github/workflows/publish.yml. - Registry: https://www.npmjs.com/package/@proof.com/x401-node
main is branch-protected: direct pushes are rejected. Bump on a branch, merge the PR, then create the Release against the exact merged commit SHA.
- Bump on a branch (no auto-tag from npm — the tag is created by
gh release createin step 4):git switch -c release-X.Y.Z origin/main npm version patch --no-git-tag-version # or minor / major; writes package.json only git commit -am "Release X.Y.Z" git push -u origin release-X.Y.Z
- Open a PR. Approve and merge in the GitHub UI.
- Locate the merged commit SHA on
mainby grepping for the release commit subject:Expect exactly one match. If zero matches, the PR isn't merged yet. If multiple, narrow the grep further.git fetch origin main SHA=$(git log origin/main --grep='Release X.Y.Z' --format=%H -n 1) echo "$SHA" # sanity-check before using
- Create the Release against that SHA —
gh release createcreates the tag automatically when it doesn't exist:gh release create vX.Y.Z --target "$SHA" --generate-notes
The Release triggers publish.yml: check suite → tag must match package.json → npm publish --provenance --access public.
Never git push --follow-tags to main: the commit is rejected but the tag still pushes, stranding it on an unmerged commit. Delete a stray tag with git push --delete origin vX.Y.Z.
- Scope is
@proof.com(with the dot), not@proof.