@openvtc/pnm-core
v0.9.1
Published
Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Wire types, WebAuthn ceremony helpers, COSE→Multikey conversion, DID verificationMethod builder, REST + DIDComm transports, mediator client, SIOP / RP-login / provision-integration flows.
Readme
@openvtc/pnm-core
Browser-side bridge between WebAuthn passkeys and VTA-managed DIDs. Lets a relying party prove the user controls a DID hosted in a remote Verifiable Trust Agent (VTA) by performing a passkey ceremony in the browser — no DID private keys ever leave the VTA, and no long-lived bearer token sits in browser storage. Speaks both REST and full DIDComm v2 to the VTA, including when the VTA is private-network and only reachable via a mediator.
This package is the shared TypeScript library that backs the Personal Network Manager browser plugin: both the PWA and the Manifest v3 extension are thin shells over it. External consumers can use it directly to build mobile companions, desktop wallets, or RP-side integrations that need the same flows without the bundled UI.
Install
npm install @openvtc/pnm-coreWhat it gives you
| Sub-module | Surface |
|---|---|
| @openvtc/pnm-core/webauthn | Passkey enrol / login ceremonies, COSE-key extraction, DID verificationMethod builder, PRF-derived secret-wrap helpers. |
| @openvtc/pnm-core/did | Multikey ↔ JWK conversion, DID-URL parsing, did:webvh log resolution. |
| @openvtc/pnm-core/vta | The VTA protocol: Trust-Task envelopes, REST/DIDComm/TSP channels, the REST auth bootstrap, passkey login as a Trust Task, and trust-task-discovery for asking an agent what it supports. Mirrors the vta-sdk Rust client's surface. |
| @openvtc/pnm-core/did-hosting | The did-management/* control plane — registering, publishing, disabling and rolling back hosted DIDs, plus domains and server instances. A hosting service, not an agent. |
| @openvtc/pnm-core/vtc | Verifiable Trust Community membership, from the member's side: apply, track, hold the credential, leave. |
| @openvtc/pnm-core/credentials | OID4VCI issuance and OID4VP presentation, plus the deferred presentations a verifier asked for while you were away. Holder-side. |
| @openvtc/pnm-core/vault | Vault Trust Tasks — list, upsert, delete, release, proxy-login, sign. |
| @openvtc/pnm-core/app-state | Agent-held key/value state scoped to a VTA context — the state a wallet needs true on every device, as distinct from /store, which is this browser profile's own. |
| @openvtc/pnm-core/admin | Agent administration: acl/*, keys/*, policy/*, consent/*, device/* (list, disable, wipe), vta/did-templates/*, vta/memory/*, vta/services/* (including drains), vta/credentials/{issue,revoke}, audit/list, config/{show,patch}, messaging/ping, session introspection, and context deletion. Operator surface — not in the root barrel, import it explicitly. |
| @openvtc/pnm-core/siop | SIOPv2 / OpenID4VP RP-side helpers. |
| @openvtc/pnm-core/provision | Sealed-bootstrap provisioning (provision/integration). |
| @openvtc/pnm-core/didcomm | DIDComm v2 packing, mediator routing, forward envelopes. |
| @openvtc/pnm-core/store | Key/value persistence (IndexedDB in a browser, in-memory elsewhere). |
| Plus | /device, /inbound, /onboarding, /rp-login, /trust-tasks, /http, /util. |
Import the module you need, not the package. Every module directory is a
published entry point, the package is marked sideEffects: false, and the
modules are layered so that lower ones never import higher ones — so
import "@openvtc/pnm-core/vta" gets you the VTA protocol without the
wallet's WebAuthn ceremonies or its IndexedDB store coming with it. Two tests
enforce this rather than trusting it (tests/package.module-boundaries.mjs,
tests/package.entry-points.mjs): one fails the build on a sideways or upward
import or a cycle, the other imports every advertised entry point in plain Node
so a stray browser global cannot reach npm.
The root src/index.ts
still re-exports everything, for callers who want it all in one import.
Minimal example — passkey enrolment
import {
beginEnrolment,
finishEnrolment,
type WebauthnEnrolmentChallenge,
} from "@openvtc/pnm-core";
// 1. Ask the VTA for an enrolment challenge for the named DID.
const challenge: WebauthnEnrolmentChallenge = await vtaClient.enrolBegin({
did: "did:webvh:example.com:alice",
});
// 2. Run the WebAuthn create() ceremony in the browser.
const credential = await beginEnrolment(challenge);
// 3. Submit the assertion. The VTA verifies it, appends the COSE
// public key as a `verificationMethod` on the WebVH log, and
// publishes the new DID-document revision.
const result = await finishEnrolment(credential, challenge.session_id);Wire compatibility
Types come from the specification, not from a copy of it
The admin/* calls take their payload types, response types and task URIs from
@openvtc/trust-tasks —
generated from the same JSON Schemas the agent's own implementation is generated
from. This package supplies the call layer: envelope, dispatch, unwrap.
Checked against the agent, not assumed
Every VTA call this library makes names a canonical Trust-Task URI, and
task-surface.json is a committed snapshot of the surface the agent actually
publishes (from vta-sdk). tests/task-surface.mjs holds the two of them
together:
- a URI this library names that the agent has never heard of — a typo, a rename, a task that moved — fails the build here rather than at a user;
- a task version the SDK has deprecated fails too, during the window where the agent still accepts it and everything appears to work;
- coverage is a recorded number (152 of 177 task families today), so a gap that grows or shrinks shows up in a diff someone reviews — and it moves when the agent grows a family just as much as when this library adds one.
Refresh the snapshot against a local checkout:
npm run tasks:sync -- /path/to/vta-sdkA second snapshot, acl-capabilities.json, covers the one part of the ACL
surface that has no binding to generate from. AclEntry.role is specified as
"an opaque role identifier interpreted by the ACL maintainer", so roles — and
the capabilities they imply — are ecosystem-local and live in ext under
org.openvtc.capabilities. src/admin/acl-capabilities.ts therefore keeps a
copy of the agent's tables, and the snapshot is what stops the copy drifting:
npm run acl:sync -- /path/to/vti-commonThe failure it guards is asymmetric and worth naming. A role whose derived set grows at the agent and not here makes the console under-report what an entry can do — an operator reading a narrowing believes it holds less than it holds, which is the misreading the agent's capability enforcement was added to end.
This package is byte-compatible with:
- The Rust
vta-sdk— typed VTA client used by thepnmCLI and other server-side consumers. - The Rust
did-hosting-client— typed WebVH hosting client. - The TypeScript
@openvtc/vti-didcomm-js— DIDComm v2 framing helpers (a runtime dependency of this package). - The TypeScript
@openvtc/trust-tasks— generated payload types for the Trust Tasks framework.
A change to the wire surface is made in dtgwg-trust-tasks-tf
first, regenerates the Rust + TS bindings, and only then lands in
this package — see the project's spec-first development discipline.
Architecture
┌──────────────┐ WebAuthn ┌────────────────┐
│ Browser │ ───────────────▶ │ Authenticator │ (Touch ID,
│ (PWA / ext) │ ◀─────────────── │ / Passkey │ Windows Hello,
└──────┬───────┘ pubkey + sig └────────────────┘ YubiKey, …)
│
│ enrol(passkey_pubkey) verify(assertion)
▼ ▲
┌──────────────┐ ┌───────┴────────┐
│ VTA │ ── WebVH update ─────▶│ Public DID doc │
│ (remote) │ │ (resolvable by │
└──────────────┘ │ any verifier) │
└────────────────┘A passkey is enrolled as a verificationMethod (purpose:
authentication) in the DID document the VTA publishes via WebVH.
Any verifier that resolves the DID can then validate a WebAuthn
assertion against the embedded public key without ever talking to
the VTA — the DID document is the trust anchor.
Browser / runtime support
- Modern browsers with WebAuthn level 2 + WebCrypto (Chrome 108+, Safari 17+, Firefox 122+).
- Node 20+ for server-side use (the WebAuthn-specific entry points are no-ops in non-browser contexts; the DID / VTA / DIDComm transports work everywhere).
ESM-only — no CommonJS build.
Versioning
Pre-1.0 (0.x) — breaking changes may land in minor bumps. The
internal contract this package depends on (@openvtc/vti-didcomm-js,
@openvtc/trust-tasks) follows the same cadence. Once the
underlying SPEC.md reaches 1.0 this package will follow.
License
Apache-2.0. See LICENSE at the repo root.
Contributing
Source lives in
OpenVTC/vta-browser-plugin
under packages/core/. See the
root README
for the workspace layout, development setup, and the smoke-test
harness.
