Wallet-request / exchange-protocol handling for the browser, Node.js, and React Native: input classification, VPR parsing, QueryByExample matching, cryptosuite negotiation, VP composition, and the VC-API / ephemeral-exchange clients.
@interop/wallet-request is the wallet side of a Verifiable Presentation
Request exchange: everything between "the user scanned or pasted something" and
"here is the signed response". It covers:
- Input classification --
classifyWalletInput/handleWalletInput, one ordered discrimination for every place a wallet accepts arbitrary text (a QR scan, a paste box, a file drop, an opened deep link): a wallet-connection payload, a client-enrollment connect code, a legacy credential-request link, a VCALM interaction URL, a registered deep link, a wallet API message, or raw credential JSON. Classification does no fetch, navigation, or storage. - Parsing and classification of exchange messages --
parse.tsturns deep links and JSON into typed messages,classify.tsturns CHAPI events and VPRs into typed requests, including the App ConnectAppConnectQueryextension. - Matching --
matching.tsfilters stored credentials against a QueryByExample, with both wallets' matchers shipped side by side. - VP composition and cryptosuite negotiation --
composeVp.tsbuilds the response presentation, embedding any grants before signing so the DIDAuth proof covers them;presentationSuite.tsnegotiates the cryptosuite. - The App Connect app-key credential --
appKey.ts: matching, minting, the store-time refusal policy. - The
WalletOnboardingQuerytransport vocabulary --onboarding.ts: the inviter's compose helper and the enrollee's classification, over a query carrying the account pointer and controller. - Exchange clients --
exchangeClient.ts(VC-API, fetch-injectable),interactionUrl.ts/interactionRequest.ts(VCALM interaction URLs),ephemeralExchange.ts(the requester's side of a WAS server's ephemeral exchange), andcapabilityRequest.ts(the zcap-only VPR a requester stores on one). processRequest.ts-- the pure request-to-response pipeline; consent and the response channel stay with the caller.
The package was extracted verbatim from @interop/wallet-core's request
subpath. The Verifiable Presentation Request vocabulary is owned by
@interop/data-integrity-core
and re-exported from the root here, so a consumer imports one package.
Two grammars this package classifies belong to a wallet's account convention
rather than to this package: the was-link wallet-connection payload and the
connect-code prefix are defined in @interop/wallet-core (space and
enrollment), beside the ceremonies that consume them. classifyWalletInput
and handleWalletInput take each as an injected recognizers predicate instead
of importing the convention. A wallet on a WAS account wires isWasLinkPayload
(from @interop/wallet-core/space) and isConnectCode (from
@interop/wallet-core/enrollment); a wallet holding no WAS account passes
neither, and those branches never match. This package depends on nothing in
@interop/wallet-core, and @interop/wallet-core imports nothing from here.
- Classification does no I/O.
classifyWalletInput,handleWalletInput,parse.ts, andclassify.tsnever fetch, navigate, or write to storage. The one network seam in the package is the injectedFetchLikethe exchange clients take. processRequestis pure. Consent and the response channel stay with the caller; zcap and App Connect processing arrive as injectedRequestProcessors.- The App Connect
appUrlorigin rule. AnAppConnectQuery'sapp.appUrlmust parse as an absolute URL, carry no fragment, and be same-origin with the attested requesting origin. An opaque origin serializes as"null"and is refused rather than compared as same-origin with itself. - Grants are signed inside the VP.
composeVp.tsembeds grants in the presentation before signing, so the DIDAuth proof covers them. - App keys are wallet-minted.
appKey.ts's store-time refusal policy rejects an imported app-key credential; only a credential this package minted may be stored. - Every field is untrusted input. VPRs, CHAPI events, and pasted or scanned text all come from outside the wallet. Helpers here validate shape and refuse malformed input rather than silently guessing at it.
- Node.js 24+ is recommended.
To install via PNPM:
pnpm install @interop/wallet-request
To install locally (for development):
git clone https://github.com/interop-alliance/wallet-request.git
cd wallet-request
pnpm install
import {
classifyWalletInput,
handleWalletInput,
classifyRequest,
composeVp,
processRequest
} from '@interop/wallet-request'
import { deepMatch } from '@interop/wallet-request/matching'A wallet on a WAS account wires the two account-convention recognizers from
@interop/wallet-core:
import { handleWalletInput } from '@interop/wallet-request'
import { isWasLinkPayload } from '@interop/wallet-core/space'
import { isConnectCode } from '@interop/wallet-core/enrollment'
await handleWalletInput({
text: scannedText,
recognizers: { isWasLink: isWasLinkPayload, isConnectCode },
handlers: {
wasLink: input => joinWasAccount(input.text),
connectCode: input => approveEnrollment(input.text),
credentials: input => resolveCredentialsInput(input.text)
// ... the remaining kinds an app implements
}
})The root exports every module. ./matching is a separate leaf export: the
QueryByExample matchers alone, so a wallet's matching tests load without the
VP-signing and document-loader graph.
PRs accepted. See CONTRIBUTING.md for editor setup (Prettier, ESLint, and EditorConfig) and how it maps to CI.
If editing the Readme, please conform to the standard-readme specification.
MIT License © 2026 Interop Alliance.