Skip to content

feat(core): adopt the Trust-Task endpoint contract - #125

Merged
stormer78 merged 1 commit into
mainfrom
feat/trust-task-endpoint-discovery
Aug 20, 2026
Merged

feat(core): adopt the Trust-Task endpoint contract#125
stormer78 merged 1 commit into
mainfrom
feat/trust-task-endpoint-discovery

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Adopts the contract settled in trust-tasks #251 (HTTPS binding 0.2) and
VTI #1020.

The contract

The HTTPS binding has always fixed the request path at /trust-tasks.
Nothing said what that path was relative to. So this plugin appended
/api/trust-tasks to its configured base while trust-tasks-https appended
/trust-tasks — and both were conformant, because there was no contract to
violate. Two implementations agreeing by convention is not a contract; it hides
the absence of one.

Binding 0.2 §6: the advertised serviceEndpoint is the Trust-Task base, and
the URL is <base> + "/trust-tasks".

The dispatcher path moves

RestChannel now defaults to /trust-tasks. The suffix lives in one
constant instead of being spelled at each site — the divergence this fixes
happened precisely because two implementations each wrote it themselves, so
anything composing a dispatcher URL goes through trustTaskUrl.

This makes vta-service 0.18.0 a hard floor

A VTA older than #1020 serves only /api/trust-tasks and will 404. There is
no fallback, deliberately, and the alternative was considered rather than
overlooked: this plugin was one of the clients keeping the superseded route's
usage metric above zero, and that metric is what governs retirement. A client
that never moves is a route that can never be retired.

Endpoint discovery (new)

vta/endpoint.ts turns a resolved DID document into a Trust-Task base, or
undefined when none is advertised — which is not an error: §6.3 keeps
out-of-band configuration conformant, and a configured base is the supported
path today.

Four behaviours are load-bearing rather than incidental:

behaviour why
matched on service type, never the id fragment the fragment is an arbitrary controller-chosen label; matching it makes interop depend on a convention nobody agreed — the same rule the Rust side applies to DIDCommMessaging/TSPTransport
a VTARest entry does not satisfy discovery the whole reason the binding defines a dedicated type — "is a VTA's REST API" and "accepts Trust Tasks over HTTPS" are different claims that coincide only because every Trust-Task server we run happens to be a VTA
non-https: is rejected, not quietly used REST is the only channel carrying a bearer; TLS in front of the receiver is why that is safe
a path prefix in the base is preserved https://host/api means the dispatcher is at https://host/api/trust-tasks — that asymmetry is the ambiguity the contract removes

DID Core's alternative shapes (type as an array, serviceEndpoint as a map or
array) are accepted, because the DIDComm entries in this codebase already use
the map form — assuming the string form from the binding's examples would reject
valid documents.

Scope

Nothing calls discovery yet. The wallet still reaches its VTA by configured
URL, which remains conformant. This lands the capability and its tests so that
adopting it later is a call site rather than a design.

Worth knowing for whenever that happens: DID-based discovery here will still hit
HOST_PERMISSION_REQUIRED, because vta-service uses an origin allowlist rather
than the permissive CORS the webvh hosting service serves. That is the existing
just-in-time permission flow, not a new constraint.

No dependency changes. Every @openvtc package is already at its newest
published version (trust-tasks 0.12.1, vti-didcomm-js 0.6.2,
vti-tsp-js 0.2.0) — reported rather than churned.

Verification

npm test539 passing, 0 failing, including package.module-boundaries
and package.entry-points. Both matter here: endpoint.ts is a new module in a
layered package, and vta/ is a published entry point that must import in plain
Node with no DOM.

Nine new tests cover the table above plus trailing-slash handling, first-usable-
entry selection, and the single-definition-of-the-suffix invariant.

The HTTPS binding fixes the request path at `/trust-tasks`, but until
binding 0.2 nothing said what that path was relative to. This plugin
appended `/api/trust-tasks` to the configured base; `trust-tasks-https`
appended `/trust-tasks`. Both were conformant, because there was no
contract — which is why it survived so long.

Binding 0.2 §6 settles it (trust-tasks #251, VTI #1020): the advertised
`serviceEndpoint` is the **Trust-Task base**, and the URL is
`<base> + "/trust-tasks"`.

**The dispatcher path moves.** `RestChannel` defaults to `/trust-tasks`.
The suffix now lives in exactly one constant rather than being spelled at
each site — the divergence being fixed here happened precisely because two
implementations each wrote it themselves, so anything composing a
dispatcher URL goes through `trustTaskUrl`.

**This makes vta-service 0.18.0 a hard floor.** A VTA older than #1020
serves only `/api/trust-tasks` and will 404. Deliberate, and taken with the
alternative understood: the plugin was one of the clients keeping the
superseded route's usage metric above zero, and that metric is what governs
retirement. A client that never moves is a route that can never be retired.

**Endpoint discovery is new** (`vta/endpoint.ts`). Given a resolved DID
document it returns the Trust-Task base, or `undefined` when none is
advertised — which is not an error, because §6.3 keeps out-of-band
configuration conformant and a configured base is the supported path today.

Four behaviours in there are load-bearing rather than incidental:

  - **Matched on service `type`, never the `id` fragment.** The fragment is
    an arbitrary controller-chosen label; matching it would make interop
    depend on a convention nobody agreed. Same rule the Rust side applies
    to `DIDCommMessaging`/`TSPTransport`.
  - **A `VTARest` entry does not satisfy discovery.** This is the whole
    reason the binding defines a dedicated type. "Is a VTA's REST API" and
    "accepts Trust Tasks over HTTPS" are different claims that coincide
    only because every Trust-Task server we run happens to be a VTA.
  - **Non-`https:` is rejected, not quietly used.** REST is the only
    channel carrying a bearer, and TLS in front of the receiver is why that
    is safe.
  - **A path prefix in the base is preserved.** `https://host/api` means
    the dispatcher is at `https://host/api/trust-tasks`. That asymmetry is
    the ambiguity the contract exists to remove.

DID Core's alternative shapes — `type` as an array, `serviceEndpoint` as a
map or array — are accepted, because the DIDComm entries in this codebase
already use the map form and assuming the string form from the binding's
examples would reject valid documents.

Nothing calls the discovery path yet; the wallet still reaches its VTA by
configured URL, which stays conformant. This lands the capability and its
tests so adopting it later is a call site, not a design.

No dependency changes: every `@openvtc` package is already at its newest
published version.

npm test: 539 passing, 0 failing, including the module-boundary and
entry-point guards.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 34205cd into main Aug 20, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/trust-task-endpoint-discovery branch August 20, 2026 15:38
@stormer78
stormer78 restored the feat/trust-task-endpoint-discovery branch August 20, 2026 15:38
@stormer78
stormer78 deleted the feat/trust-task-endpoint-discovery branch August 20, 2026 15:38
stormer78 added a commit that referenced this pull request Aug 26, 2026
trust-tasks #279 re-cased ~200 extended error codes to the lowerCamelCase
SPEC §4.10 rule 4 requires: `provision/integration:context_required` became
`provision/integration:contextRequired`. Only the local part after the `:`
moved; namespaces are unchanged, and retired specs were deliberately left
snake_case.

`onboard-view.tsx` gated the onboarding context picker on a `===` against the
old spelling. Against a VTA that has taken #279 that comparison goes false and
nothing else happens: no error, no crash, no log — the picker just stops
appearing, and an operator with a multi-context VTA is dead-ended at Connect
with a message about a context they were never offered a way to choose.

Swapping the literal would move the defect rather than fix it. This wallet is
on the matching side of the wire, never the declaring side, and it updates on
the Chrome Web Store's schedule while the VTA updates on its own. A wallet
installed today still talks to a months-old agent next year; a wallet that has
not auto-updated meets an agent that took #279 this morning. Either literal is
correct for exactly half the deployed fleet, so both spellings have to match.

`matchesTrustTaskCode(actual, canonical)` (`trust-tasks/error-code.ts`) takes
the canonical camelCase code and accepts either spelling, deriving the
snake_case form rather than carrying a hand-maintained pair. Call sites read as
the code the registry declares today and the compatibility lives in one place,
so retiring the fold is one edit rather than a sweep — its TODO names the
condition: a declared minimum vta-service floor at or above the release
carrying the re-cased codes, the way #125 made 0.18.0 a floor for the
dispatcher path. It is deliberately not a fuzzy compare; codes differing by
more than the §4.10 re-casing stay distinct, so a rename cannot become a
collision.

`PROVISION_CONTEXT_REQUIRED` is exported from `provision/send.ts` in the
registry's current spelling, following the `MEDIATOR_REQUIRED` precedent in
`bridge-protocol.ts`. The code crosses two message-passing hops (offscreen →
background → popup) and is forwarded verbatim at each: normalising in transit
would make the hops need redeploying in lockstep with the agent, and would hide
from the popup which side of the rename its peer is on.

The other three codes this repo names — `vault/delete:versionConflict`,
`vault/sign-trust-task:notSignable`, `vault/upsert:sealedSecretInvalid` — all
verified against the registry, and all appear only in doc comments. Those are
re-spelled to the current declaration with a note pointing at the matcher, so
the next site that starts branching on one starts from the right shape.

Left alone deliberately: `auth:consent_required` is a `details.reason` token,
not an extended error code (the registry declares no bare `auth:` namespace),
`e.p.msg.context_required` is a DIDComm problem-report code from a different
scheme, and `push/register:*` is still snake_case in the registry.

Seven new tests pin the dual-accept property itself, since that is what makes
the deploy order safe and a comment cannot hold it: an equality that goes false
raises nothing.

Lint (`tsc -b`) clean across all workspaces; build clean; 546 tests passing,
0 failing (was 539). Both CI bundle guards re-run by hand and pass.

**Ship this before the vta-service PR that emits the new spelling** (R3.7).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant