vta-browser-plugin — the VTA Wallet in the browser
Repo: github.com/OpenVTC/vta-browser-plugin — npm scope @openvtc/pnm-*
Passkeys solve local authentication; DIDs solve global identity; neither alone lets a person log into a third-party website as the controller of their VTA-held DID. The browser plugin is the bridge: it proves control of a DID hosted in a remote VTA by performing a passkey ceremony in the browser — no DID private key ever leaves the VTA, and no long-lived bearer token sits in browser storage. Its first milestone (May 2026) enrolled a passkey as an authentication verification method in the VTA’s did:webvh document, so any relying party can verify a WebAuthn assertion by DID resolution alone (the server half of that idea is VTI’s vti-webauthn crate, a DID-VM-resolved assertion verifier).
Since then it has grown into three things in one MV3 extension. The VTA Wallet: the browser-resident Personal Network Manager (see vta-topology) that holds holder identities, keeps one persistent mediator inbound session per onboarded agent, and renders consent / step-up approvals for VTA-gated operations — the in-browser sibling of the iOS mobile agent. The management console (September 2026): everything an operator used to do to their agent from the pnm CLI — contexts, keys, DIDs, ACL, approvals, policy, transports, audit, and since Eucalyptus the mediator carrying the agent’s mail — reachable from a browser tab, composed in the console and signed in the wallet’s offscreen document so the console itself holds no key material. And the persona identity map (attributes, faces, contexts, contacts, disclosure history) and data rooms panes, the first user-facing surfaces for the VTA’s persona and rooms task families. At the VTI-Eucalyptus tag (2026-10-07) the project’s own release arc summed the Dogwood → Eucalyptus window up as “a VTA management console with proof-of-presence step-up, faces manager, rooms, TSP Rev 3 in tsp-js, sign-in over auth/authenticate/0.2” — the first time the plugin carries a final coordinated tag since Dogwood (coordinated-releases).
Not to be confused with pnm-server: that is the headless build of VTI’s Rust pnm CLI (secrets in plaintext config rather than an OS keyring), downloaded from download.firstperson.dev/pnm-server/… in the vti-setup guides. Nothing in this repo produces it.
Components
npm-workspaces monorepo (Node ≥ 24). Versions at the coordinated-release tags:
| Package | Cypress (08-17) | VTI-Dogwood (08-31) | VTI-Dogwood-R1 (09-01) | VTI-Eucalyptus (10-07) | Role |
|---|---|---|---|---|---|
@openvtc/pnm-core | 0.4.0 | 0.6.0 | 0.6.0 | 0.9.1 (+ unreleased) | The library — a standalone, strictly layered VTA client. WalletSession.bootstrap(), WebAuthn ceremony + COSE→Multikey, RP sign-in (three shapes, below), vault, provisioning, Trust Tasks, inbound handling (persist-before-ack, reconnect scheduler, verified senders), the three transport channels (REST / DIDComm / TSP). Since 0.6.0 every channel signs every outbound Trust-Task document and takes a `SigningIdentity |
@openvtc/pnm-extension | 0.2.0 | 0.2.0 | 0.2.0 | 0.2.0 | MV3 Chrome extension (manifest name “VTA Wallet”; the root pnm-browser-plugin package is 0.1.2, the number the Eucalyptus release notes quote): popup, options/setup, offscreen inbound sessions, consent windows, and since #155 manager.html — the management console, built as its own Vite bundle so CI can assert that admin/* and messaging/* task URIs appear in manager.js (and the offscreen entry) and nowhere else, and that vta/seeds/* (mnemonic export) appears nowhere at all. Injects window.vtaWallet into granted origins: login, loginDidcomm, proxyLogin, walletProfile, walletDefaults, signTrustTask, requestTask, stepUpVta, vaultList, apiGet/apiPost, mediatorStatus, and since October the per-community approver quartet approverIdentity, approveStepUp, attestApprover, approveDecision. Optional host permissions only, no static content scripts, no cookies permission (CI-asserted). Chrome Web Store packaging exists (#119) but the extension has still never been submitted or published — a fact the repo’s CLAUDE.md turns into a rule: nothing is deployed, so no compatibility folds. |
@openvtc/vti-tsp-js | 0.2.0 | 0.2.0 | 0.2.0 | 0.3.0 (+ unreleased) | Pure-TypeScript TSP: HPKE / Ed25519 / X25519 via @noble (no WASM), CESR framing, Direct / Nested / Routed modes, RFC 9180 vectors in CI. 0.3.0 (#253, 2026-09-15) is spec Rev 3: packs Rev 3 only, reads Rev 3 and Rev 2 via a frozen decode-only codec; adds relationship control messages (XRFI/XRFA/cancel), the §7.2/7.3 state machine, and — from an external contributor (Mickens-Lab, #233) — pluggable SigningKey / KeyAgreement for non-exporting key custody. Unreleased since: resolveAccept (does a received accept answer our outstanding invite, §7.2.2) and a test-only unsafe-testing subpath for byte-reproducible packing. |
@openvtc/pnm-pwa | 0.2.0 | 0.2.0 | 0.2.0 | 0.2.0 | Vite + React wallet PWA with the /smokes diagnostic page. |
@openvtc/pnm-demo-rp, pnm-reviewer-demo | 0.1.0 | 0.1.0 | 0.1.0 | 0.1.0 | A test website exercising every wallet login path (#140), and a Web Store reviewer bootstrap. |
How the Web Login Family Fits Together
web page ──window.vtaWallet.{login | proxyLogin | walletProfile | requestTask | approveStepUp}──▶ extension
extension ──▶ @openvtc/pnm-core (Trust-Task envelopes; channel signs every document; origin → RP pin)
──▶ @openvtc/vti-tsp-js (TSP Rev 3 frames; Trust Task inside the tsp/0.1 binding envelope) ─┐
──▶ @openvtc/vti-didcomm-js (authcrypt, verified sender; routing/2.0 forward; mediator WS) ─┼──▶ VTA / RP
──▶ REST ({base}/trust-tasks, the HTTPS binding; net-guard vets the base URL) ─┘
◀── every reply's Data Integrity proof verified, its signer checked against the agent addressed (#215),
and since #278 a reply is believed only from the peer the transport *authenticated*
RP server ──▶ verifies the auth/authenticate/0.2 response (or, for proxyLogin, the id_token) by resolving the persona DID
manager.html ──RUNTIME_MANAGER_TASK / RUNTIME_MEDIATOR──▶ offscreen mints + signs ──▶ same channels (console holds no key)
Transport preference is TSP > DIDComm > REST, but since #143 the UI reports the transport that actually carried a byte (“advertisement is not availability”). The vti-didcomm-js floors are correctness constraints, not preferences: ^0.6.2 for hand-off-then-ack (R1.6), ^0.10.0 for reading long-framed (--E) TSP replies, ^0.11.0 for a mediator’s own Trust-Task replies to be acked rather than pile up in the holder’s queue, and ^0.12.0 for the authenticated VerifiedSender the wallet now authorizes on. REST survives Eucalyptus as a transport — Trust Tasks posted to {base}/trust-tasks, the HTTPS binding — but the bespoke REST endpoints it used to depend on (/auth/step-up/vta/*, the SIOPv2 id_token envelope) are gone, replaced by Trust Tasks that ride any channel.
Three sign-in shapes, all of which sign in as a per-site persona rather than the wallet’s own holder DID (#144–#146, VTI-Dogwood): login — since #284 (09-28) the canonical auth/challenge → auth/authenticate/0.2 pair as Trust Tasks to the RP’s {baseUrl}/trust-tasks, every reply required to be signed by the RP pinned for that origin; the page may hand over a did:key session key, which the wallet puts inside the document the subject signs and which the RP must echo, so the page can sign its own session requests afterwards without the wallet (such a sign-in is always asked, even for a remembered site). This replaced the self-issued SIOPv2 id_token, and the SIOP login client was removed from the extension — the siop/ module stays in pnm-core only for the vault’s didSelfIssued entries. VTA-proxied login (proxyLogin({}) resolve-or-bind; vault/proxy-login/0.1 mints the token with a vault-held key that never leaves the VTA; walletProfile({target}) answers “who does this site know me as?” without enumerating the vault). Trust-Task login over the wallet’s own transport (loginDidcomm, since #141 the same auth/* pair over whichever transport the RP advertises, signed as the persona by vault/sign-trust-task/0.2). Around all three sits the origin → RP pin (#281, “M5”): a pin is seeded only by a login the human approved, looked up by exact attested origin, and signTrustTask / stepUpVta refuse any recipient or base URL other than the pinned one.
Recent Development
- Eucalyptus final (2026-09-18 → 10-07, 83 commits, #257–#295; tagged
VTI-Eucalyptus10-07 at d6df679) — the three weeks that turned the September console into something a community can govern with. Six threads.- Faces manager — the persona console completed (#262–#271, 09-19 → 09-23; trust-tasks 0.19.14 → 0.19.20, vta-sdk 0.45.1 → 0.47.0). The identity map learned the parts of persona the September pass left as SDK calls with no surface: links between faces that no attribute holds (#263); the lifecycle answers — who holds an old value, where an edit landed, what a context may call a face (#264); a face saying which of its names is its own (
displayNameslot) and kept old values a pinned face depends on, withpersona/attribute/purge-versionbehind a confirmation that says the pinned faces will show nothing (#265); make a face where it is asked for, retire it, reinstate it, read its history —persona/profile/composewith a per-value correlation check before the write,usage,timeline,retire/reinstate, face reach, bindinguntil(#268); then contacts (persona/contact/*, what others have told the holder), formats (persona/renderers/list, “what shape did it leave in”) and local faces made inside a context (#270), which claim no provenance (#271). Alongside, the ACL table caught up with vti-common:key-export(VTI #1619) and the additivepersona-holder, and a “Narrow with nothing ticked” that used to send[]— which the agent reads as clear the narrowing, the whole role — is now refused (#267). - Mediator Lens and QR (#273–#276, 09-23/24). A mediator serves its own
messaging/*operations (statistics, every account’s queue, one account’s mail, a live traffic monitor) as Trust Tasks addressed to itself, and the wallet already holds an authenticated session with it for each agent’s inbox — so the console can look inside the relay as the agent’s holder with no new socket, no second CORS allowance and no key export.pnm-core/mediator, an allow-listed offscreen channel (noconfig/patch, nomessage/get), a per-port monitor lease, a preview-then-confirm purge that refuses a stale plan, and thevti-didcomm-js0.11.0 floor it needed (replies never acked, refusals surfacing as timeouts, monitor batches hitting the persist-before-ack path — all three reproduced against a real mediator). Then every DID gets a QR button (#275/#276): the bare DID as UTF-8, level M, black on white in every theme (phone cameras refuse inverted codes), the same encoding as the VTC landing page andpnm vta qr— so the Keyring phone wallet, or any wallet, can take a DID by scanning it. - Verified senders (#278, 09-25;
vti-didcomm-js0.12.0). A task-consent outcome — the message that can raise “The VTA refused your approval” and drop the decision the wallet is waiting on — was believed when its plaintextfromnamed any enrolled executor. It is now keyed on the sender the transport authenticated (the authcryptskid’s DID, or the senderunpackInboundTspproved), must be the executor the decision was sent to, and that identity is persisted with the pending inbound so an MV3 teardown cannot launder it.sendAndAwaitReplytakes a requiredfrom(a thread id is not a secret), and sender keys are matched by exact key id. - Step-up and sign-in become Trust Tasks (#280–#284, 09-27/28).
stepUpVtaat a did-hosting relying party is nowauth/step-up/start/0.1→ verify the RP-signedapprove-request/0.3inside → signedapprove-response/0.5→auth/refresh/0.1, all posted to{base}/trust-tasks; the bespoke/auth/step-up/vta/{start,finish}paths and their snake_case parsing are gone (#280). What a page can have the wallet sign is bounded — no unaddressed documents, nothing addressed to the holder’s own agent, no step-up approve-* (#281), and documents a page asks for are signed forauthentication, the holder’s own request, since the control plane refuses anassertionMethodproof on them. Thenloginoverauth/authenticate/0.2with a page session key (#284, above), the SIOP client deleted, and page-signed sign-ins refused. - Console and onboarding fixes (#282–#290, 09-28 → 10-02). The printed onboarding grant became
--expires 1h --handoff: since vta-service #1738 an entry may not write one that outlives it (VTI-ACL-053), so the expiring ephemeral could no longer write the wallet’s permanent admin; the one-time hand-off marker (VTI-ACL-054, pnm-cli ≥ 0.25.0) is the VTA’s answer (#282). Edit an ACL entry’s label and expiry, group a context’s access list (#283); grantpersona-holderin the setup command and read it back (#285); a task the VTA relays onward (did:webvh creates, room key reads,vault/proxy-login,provision/integration) gets ≥ 80 s — the budgetvta_sdk::budgetderives — instead of the channel’s 30 s, which had made a DID “time out” while its create succeeded (#287);auth/revoke-session/0.2(VTI #1790); clear a context’s DID overvta/contexts/update-did/1.1(#288). AndsignTrustTaskas a VTA-held persona now refuses withsign-trust-task/as-did-unavailableinstead of silently signing with the holder key behind aconsole.warn(#289), and reaches the VTA over TSP or DIDComm when no REST base is known (#290) — both found wiring the VTC console’s wallet-authorized signing-key enrollment (auth/signing-key/enroll/0.2). - A per-community approver (#291–#293, 10-03; trust-tasks 0.23.0). The wallet half of VTI 1920:
approverIdentity({audience})mints one approverdid:keyper relying party so communities cannot correlate the user, each sealed under the approver KEK rather than HKDF-derived from one root (a derived key cannot be named without the gesture, and deleting it forgets nothing);approveStepUpchecks the request names this approver and thatboundToequals the recomputedvtc/step-up/v1digest, shows the reason, operation and community, takes the biometric, and returns a signedauth/step-up/approver/attest/0.1;approveDecisionvouches for a VTC administrator’stask-consent/decision/0.2— approve or deny — after recomputing thevta/task-consent/v1payload digest, refusing on a mismatch before any prompt. Every statement is single-use, pinned to the origin’s login, and spent before the matching approve-response is signed. #292 took the generated types for both and raised the trust-tasks floor to the first binding whosegit-ns/activity/list/0.1schema is satisfiable.
- Housekeeping: Dependabot bumps (#257–#260, #295: Vite 8.3.1, React 19.3, lucide 1.45), a context delete that shows the subtree it destroys (#257), two startup races (#277), an Approve that could be settled as a Deny by its own window closing (#272).
- Faces manager — the persona console completed (#262–#271, 09-19 → 09-23; trust-tasks 0.19.14 → 0.19.20, vta-sdk 0.45.1 → 0.47.0). The identity map learned the parts of persona the September pass left as SDK calls with no surface: links between faces that no attribute holds (#263); the lifecycle answers — who holds an old value, where an edit landed, what a context may call a face (#264); a face saying which of its names is its own (
- Dogwood → Eucalyptus RC (2026-08-19 → 09-18, ~275 commits, #120–#255) — the busiest month in the repo’s life, in three distinct phases.
- Towards
VTI-Dogwood(08-19 → 08-31; pnm-core 0.4.0 → 0.5.0 → 0.6.0). Why: VTI #1146 turned on the four checks a Trust Task specification declares for itself, and 93 of the 141 task types the wallet spoke required aproof— none had one, so everyvault/*,acl/*,vta/webvh/*andvtc/*call was being refused. #131 signs every outbound document at the channel and makes the signer a required constructor input so an unsigned channel cannot compile. Alongside: the did:webvh DID lifecycle and contexts (#123), the Trust-Task endpoint contract (#125), Trust Tasks 0.16 with full JSON-Schema validation of inbound consent payloads via@cfworker/json-schema(#127/#128/#130), provisioning over any advertised transport (#133), executor-initiated requests over TSP as well as DIDComm (#134), transport health reporting (#143), a real demo RP website (#140), Trust-Task login over any transport (#141), and the per-site persona login trilogy (#144–#146). Tagged 08-31 with no GitHub Release. VTI-Dogwood-R1(08-31 → 09-01; #147–#156). An operator running several VTAs found that the wallet had a single wallet-widemediatorDidinbox, so every agent but one had its consent requests silently lost. #148–#150 make the inbox per agent, derived from each agent’s DID document; #153 fixed thetask-consent/grantednotice parsed at a pre-spec shape; #154 a single-use replay exemption for a payload already approved twice.- After R1 (09-01 → 09-18; pnm-core 0.7.0 → 0.9.1, tsp-js 0.3.0). The management console (#155/#157): nine panes on the Trust Context tree, proof of presence (a WebAuthn assertion) in front of irreversible controls,
adminScopeat setup (#187); pnm-core 0.7.0 generated wire types, 0.9.0 ACL capability narrowing (VTI #1279), 0.9.1 republished after a partialdist. Persona (#159–#221): the disclosure consent screen, the identity map, worlds, masked sensitive values. Data rooms (#204–#231): mint a room’s DID first, host DIDs, invitation / membership / authority credentials, records, epoch-chain repair. Security and wire (#232–#255): the cross-repo SEC-4045 egress review throughvti-didcomm-js0.8’snet-guard, every reply’s proof verified and bound to the addressed agent (#215), Trust Tasks inside thetsp/0.1binding envelope (#246), TSP Rev 3 (#253), long frames (#254) and the single-label host guard (#255).
- Towards
- May 2026 (~90 commits) — scaffold, DIDComm stack, SIOPv2 login,
window.vtaWalletprovider, vault M1/M2, onboarding via ephemeral did:key →acl/swap-key, WebAuthn-PRF holder-secret encryption, multi-VTA, first npm publish under@openvtc. - June — Trust Tasks 0.2 migration, pnm-core 0.2.0, Web Push wake-up (registering with the
vti-push-gateway), camelCase wire, bearer caching / 401 retry. - July — transport-agnostic trust tasks +
vti-tsp-js(#75–#81); genericrequestTaskrelay (#84); approver identity with a PRF-gated signing key and a single-browser biometric approver (#90/#91); co-located approver relay (#92/#93); the D8 remediation batch — bounded fetches, approver-inbox backoff, durably record inbound before mediator ack (R1.6, #101), parse error bodies before throwing; Vite 8 / TS 7. - August (to Cypress) — consent robustness (#104–#113); pnm-core 0.3.0 → 0.4.0; tsp-js 0.2.0 pure-TS HPKE (#116/#117); Web Store packaging, per-site permissions, wallet UI (#119); standalone layered library + cookies removal (#121).
- Direction: still open after Eucalyptus — Chrome Web Store submission (unchanged since May), publishing the post-0.9.1 pnm-core and post-0.3.0 tsp-js changes to npm, extracting pnm-core to its own repo, an
allowHostspin list so a public name resolving to a private address is also refused, deleting the Rev 2 TSP codec once the last Rev 2 peer is gone, a room that can answer as its own DID over a mediator, and a relying-party-side verifier for theauth/authenticate/0.2sign-in (see rp-sdk-js).
See also: verifiable-trust-agent, vti-didcomm-js, rp-sdk-js, affinidi-webvh-service, didcomm, trust-spanning-protocol, trust-tasks, vta-topology, coordinated-releases