feat(extension): TSP transport for trust tasks (TSP > DIDComm > REST) - #79
Merged
Conversation
Route trust tasks over TSP (Trust Spanning Protocol) when a VTA advertises a `TSPTransport` service, ahead of DIDComm and REST. TSP is sender- authenticated by its envelope (like DIDComm authcrypt), so it carries the same canonical Trust-Task envelope with no bearer. Transport: TSP rides the SAME warm mediator socket as DIDComm — the mediator multiplexes both (binary 0xF8 -> TSP, text -> DIDComm), so there is no second socket and no one-socket-per-DID conflict with the wallet's DIDComm inbox. `MediatorSessionTspTransport` sends the packed envelope as a binary frame and awaits the sealed reply (FIFO; a VtaSession drives one request at a time). Reply frames arrive back on the same session via the new `onTspFrame` demux (requires @openvtc/vti-didcomm-js ^0.6.0). - pnm-core: MediatorConnection gains `sendBinary` + `awaitTspFrame`; `MediatorSessionTspTransport`; `resolveVtaServices` detects the type-based `TSPTransport` service; `resolveVtaTspEndpoint` + `tspHolderIdentityFromSecret` (X25519 derived from the Ed25519 root, matching the holder did:peer keyAgreement key). - offscreen `getVtaSession`: builds the TSP channel from the pooled warm session, first in the priority chain. - Error phasing: a send failure (pre-send, nothing reached the VTA) is `e.client.unsupported` -> safe fallback to DIDComm; a reply timeout (post-send, a mutation may have applied) is `e.client.network` -> hard fail, no retry. - `preferTsp` setting (default ON; options-page toggle) to pin a VTA to DIDComm/REST if a mediator's TSP delivery misbehaves. - Popup transports label shows TSP (backfilled onto existing connections by the transport refresh — no re-onboard). Live-validated against a TSP-enabled VTA + mediator: vault list, proxy- login (sealed session blob), add (mutation + sealed secret), and delete all round-trip over TSP, with the DIDComm inbox coexisting on the one socket. Adds `MediatorSessionTspTransport` unit tests. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
added a commit
that referenced
this pull request
Aug 29, 2026
Provisioning was the last VTA operation with no transport chain. Every other one — vault, contexts, dids, device wake, acl — has gone through `TrustTaskSender` since #79, so it runs over TSP, DIDComm or REST according to what the agent publishes. `runProvisionIntegration` still took a `DidcommMessageBridge`, and `offscreen.ts` built its own mediator session rather than calling `getVtaSession`. A wallet already connected over TSP had to open a DIDComm session to onboard, and a VTA advertising no DIDComm could not be onboarded at all. The VTA has served this operation through the shared dispatcher for some time (`TASK_PROVISION_INTEGRATION_0_3` → `trust_tasks/provision_integration.rs`), taking the same request body and returning the same response body as the bespoke DIDComm handler beside it. We were using the bespoke one, and paid for it a second way: that handler labels its reply from a hand-written version→URI map which was not moved when the router cut over to 0.3, so a provisioning that had fully succeeded — bundle sealed, admin rolled over, secret written — came back labelled `provision/integration/0.1#response` and was rejected here as an unexpected reply type. The dispatcher has no such map; it sets the `#response` fragment on the request URI it just parsed, so the class of bug does not exist on that path. (VTI #1202 fixes the map itself, for the CLI clients still on it.) `sendProvisionIntegration` is now `buildTrustTask` + `sender.send`, and reads almost exactly like `swapAcl`, which has been the template for this shape since the ACL swap was collapsed. **The session speaks as the ephemeral.** The holder is what provisioning is about to mint, so it cannot exist yet, and the warm mediator pool authenticates as the holder — it calls `loadHolder` itself. `getVtaSession` therefore splits: `buildVtaSession` takes the identity and a `MediatorConnector`, and `getVtaSession` is the holder-over-the-warm-pool case of it. Onboarding passes the ephemeral and connections it opens and closes itself. Handing the ephemeral a pooled connection would have sent its request under a DID the operator never granted, which is the failure this split exists to make unrepresentable. Authorisation is unchanged and needed no VTA-side work: `auth_from_message` is `auth_from_did(sender)`, and the dispatcher's trust-task path resolves claims the same way, so the ACL grant the operator just made authenticates the call identically whichever transport carries it. **DIDComm leads for onboarding, deliberately.** The chain is otherwise TSP-first. But a TSP reply timeout is a hard failure by design — a mutation may already have applied, and provisioning is as mutating as an operation gets — and TSP delivery to a *just-granted ephemeral* has never been exercised: the enrolled holder's mailbox is live-validated, the ephemeral's is a transient auth session, and the one headless attempt against a mailbox-less did:key timed out. Leading with DIDComm leaves the proven path exactly as it was while still letting a VTA that advertises no DIDComm onboard over TSP or REST. Promote TSP once an ephemeral round-trip is confirmed against a live mediator. Three consequences worth naming: - **A mediator is no longer mandatory.** `MediatorRequiredError` now fires only when a mediator is the sole remaining possibility and none was supplied, rather than before any transport is considered. - **A mediator that will not connect skips its channel** instead of failing the session, mirroring what the TSP branch already did. Safe for the same reason: pre-send, so nothing has been dispatched and nothing can have been applied twice — the distinction `VtaSession` draws when it falls back on `e.client.unsupported` but never on a post-send failure. - **The request body is lowerCamelCase** (`createContext`), the canonical 0.2+ form the registry declares and the generated payload type now enforces. The Rust struct still carries snake_case aliases, so the old spelling would also be accepted; that is a fold on the VTA's side, not a reason to keep sending the legacy form. The signed VP is relayed byte-for-byte, as always. `ProvisionProblemReportError` is gone with the DIDComm-specific error channel. `provisionRefusalOf` replaces it, reading the code and `details.candidates` off `VtaClientError.details` — fields, not a parsed message (R3.7). The popup's context picker is untouched: offscreen still forwards `{code, candidates}` and `onboard-view` still compares against `PROVISION_CONTEXT_REQUIRED`. Needs VTI #1204, which makes the dispatcher emit that canonical code with structured candidates. Without it the spine refuses an ambiguous context as `malformedRequest` with the candidates rendered into a sentence, and the picker would have nothing to read — trading a working recovery UX for a string match. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Route trust tasks over TSP (Trust Spanning Protocol) when a VTA advertises a
TSPTransportservice, ahead of DIDComm and REST. TSP is sender-authenticated by its envelope (like DIDComm authcrypt), so it carries the same canonical Trust-Task envelope with no bearer.Transport: one socket, multiplexed
TSP rides the same warm mediator socket as DIDComm — the mediator multiplexes both (binary
0xF8→ TSP, text → DIDComm), so there is no second socket and no one-socket-per-DID conflict with the wallet's DIDComm inbox.MediatorSessionTspTransportsends the packed envelope as a binary frame and awaits the sealed reply (FIFO — aVtaSessiondrives one request at a time). Replies arrive back on the same session via the newonTspFramedemux.Changes
MediatorConnectiongainssendBinary+awaitTspFrame; newMediatorSessionTspTransport;resolveVtaServicesdetects the type-basedTSPTransportservice;resolveVtaTspEndpoint+tspHolderIdentityFromSecret(X25519 derived from the Ed25519 root, matching the holderdid:peerkeyAgreement key).getVtaSession: builds the TSP channel from the pooled warm session, first in the priority chain.e.client.unsupported→ safe fallback to DIDComm; a reply timeout (post-send, a mutation may have applied) →e.client.network→ hard fail, no retry.preferTspsetting (default ON; options-page toggle) to pin a VTA to DIDComm/REST if a mediator's TSP delivery misbehaves.Validation
Live against a TSP-enabled VTA + mediator: vault list, proxy-login (sealed session blob), add (mutation + sealed secret), and delete all round-trip over TSP, with the DIDComm inbox coexisting on the one socket. Core suite: 61 pass (adds
MediatorSessionTspTransporttests).Merge order
Merge + publish
@openvtc/vti-didcomm-js@0.6.0first (#4), then this.