@openvtc/trust-tasks
v0.19.21
Published
Generated TypeScript bindings for the Trust Tasks framework registry.
Readme
@openvtc/trust-tasks
TypeScript bindings and the consumer pipeline for the Trust Tasks framework registry.
Two halves: generated types for every specification in the registry, and a hand-written implementation of the SPEC.md §7.2 checks that decide whether an inbound document may be acted on. See Consuming a document — types alone will not make a conforming consumer.
Every spec under dtgwg-trust-tasks-tf/specs/
that defines a payload.schema.json is compiled — via
scripts/build-ts-bindings.mjs (json-schema-to-typescript) — to a
TypeScript module under
src/<slug>/<version>/payload.ts,
giving you a typed Payload and (where the spec defines one)
Response interface for each Trust Task. The package's root
index.ts re-exports every module under a stable name keyed by the
spec slug.
The Rust counterpart is trust-tasks-rs;
both crates are regenerated together so the Rust and TypeScript wire
shapes stay byte-identical.
Install
npm install @openvtc/trust-tasksUsage
import { AclGrant_v0_1, DidManagementDidRegister_v0_1 } from "@openvtc/trust-tasks";
// Compose a request payload, type-checked against the spec's
// payload.schema.json. `Payload` is a stable alias every module
// exports, so you never have to look up the title-derived interface
// name (here, `ACLGrantPayload`).
const grant: AclGrant_v0_1.Payload = {
entry: {
subject: "did:key:z6MkAlice",
role: "admin",
},
reason: "onboarding",
};
// Each module exports the Response sub-schema too when the spec
// defines one — the framework's #response fragment convention.
// Fire-and-forget specs export neither `Response` nor
// `RESPONSE_TYPE_URI`, because SPEC.md §4.4.1 forbids emitting a
// `#response` document for them.
const reply: AclGrant_v0_1.Response = {
entry: { /* … */ },
};
// did-management example.
const register: DidManagementDidRegister_v0_1.Payload = {
path: "alice",
method: "webvh",
didData: "{\"versionId\":\"1-…\",…}",
};For the full list of generated modules, see src/index.ts
or the website index.
Module layout
Each spec lands at a stable path:
src/
├── acl/grant/0.1/payload.ts // AclGrant_v0_1
├── auth/authenticate/0.1/payload.ts // AuthAuthenticate_v0_1
├── did-management/did/register/0.1/
│ └── payload.ts // DidManagementDidRegister_v0_1
├── vault/proxy-login/0.1/payload.ts // VaultProxyLogin_v0_1
└── …Slug / separators in the URI map to directory boundaries; the
exported name is generated by Pascal-casing the slug segments and
appending _v<MAJOR>_<MINOR>. Multiple versions of the same spec
land in sibling directories (0.1/, 1.0/, …) and export
distinct names — there is no "latest" alias by design.
Shared definitions
A definition several specs $ref — Ext (the SPEC §4.5.1 extension object),
AclEntry, VaultEntry, DigestMultibase — is declared once, in
src/_shared/components.ts, and re-exported from every spec module that uses
it under the name that module has always used. So AclGrant_v0_1.AclEntry and
AclShow_v0_1.AclEntry are the same type, not two copies of one shape:
import { SharedComponents, type AclGrant_v0_1 } from "@openvtc/trust-tasks";
// Write a helper once, over the shared definition, and it takes any spec's.
const namespaces = (ext: SharedComponents.Ext) => Object.keys(ext);Where one definition name covers more than one shape — VaultEntry differs
between vault/_shared/0.1, 0.2 and 0.3 — components.ts qualifies each
(VaultEntry_VaultV0_1, …) rather than pretending they are interchangeable.
The spec modules still export the bare VaultEntry, bound to the version they
were generated against.
Before 0.15.0 these definitions were copied into every module that referenced
them, and repeated copies inside one module were numbered Ext1, Ext2, Vid1
and so on. Those numbered names are gone; see
CHANGELOG.md for the one-line migration.
Consuming a document
A conforming consumer applies all eight checks in
SPEC.md §7.2 before
acting on an inbound document. Three of them — recipient-REQUIRED, proof-REQUIRED
and audience binding — are declared per specification and cannot be derived
from the document itself, so every generated module exports the declarations as
SPEC (and RESPONSE_SPEC, where the spec defines a response).
consumeInbound runs item 2 and items 4–8, the freshness bound, and item 11's
duplicate-execution record, then calls your handler. Items 1 and 3 (framework
schema, unknown type) belong to your parse and dispatch, and have already
succeeded by the time you hold a typed document.
import {
consequentialChecks,
consumeInbound,
InMemoryReplayGuard,
respondWith,
StaticTransport,
AclGrant_v0_1,
} from "@openvtc/trust-tasks";
// The guard *is* the duplicate-execution record — one per consumer, held for
// the process's lifetime. Back it with a shared store if you run replicas.
const guard = new InMemoryReplayGuard();
const outcome = await consumeInbound<AclGrant_v0_1.Payload, AclGrant_v0_1.Response>({
transport: new StaticTransport({ issuer: peerVid }), // what the transport authenticated
spec: AclGrant_v0_1.SPEC,
proofPolicy: { kind: "verify", verify: myVerifier },
payloadPolicy: { kind: "validate", validate: myValidator }, // or acceptUnvalidated
// acl/grant is consequential: a replayed envelope must not grant twice.
checks: consequentialChecks(guard), // or notConsequentialChecks()
doc,
myVid: "did:web:maintainer.example",
now: Date.now(),
newErrorId: () => crypto.randomUUID(),
handler: async (accepted, parties) =>
respondWith(accepted, crypto.randomUUID(), await applyGrant(accepted.payload, parties)),
});
switch (outcome.kind) {
case "handled":
return send(outcome.response);
case "rejected":
return send(outcome.error); // already addressed per §8.1
case "suppressed":
// §8.1: an `identityMismatch` the transport cannot safely answer. Emitting
// anything here would be an oracle. Log it — silent is the rule, invisible
// is a footgun.
return log(outcome.reason);
case "duplicate":
// §7.2 item 11: this document already executed. Not an error — return the
// prior result where there is one, otherwise emit nothing.
return outcome.priorResponse === undefined ? undefined : send(outcome.priorResponse);
case "accepted":
return undefined; // fire-and-forget: nothing to emit
}Choose the checks deliberately. consequentialChecks(guard) is correct for
any task whose execution grants access, moves value, discloses a secret, or is
otherwise irreversible — SPEC §7.2 item 11 makes duplicate-execution protection
normative for those, and every transport binding delegates it to the consumer.
notConsequentialChecks() keeps no record and is conformant only where the task
is not consequential, or where the specification declares repeated execution
safe and intended.
Choose a proof policy deliberately. { kind: "verify" } honours in-band
proofs. { kind: "rejectIfPresent" } is for consumers with integrity from
another layer — it refuses a proof-bearing document rather than silently
dropping the proof, which would mislead the producer about the guarantees of
the exchange. { kind: "acceptUnverified" } is the explicit opt-out and only
safe where the transport already provides equivalent end-to-end integrity. A
specification that declares proof REQUIRED rejects a proofless document under
all three.
The pipeline mirrors consume_inbound in
trust-tasks-rs check for check, so
a TypeScript consumer and a Rust one reach the same verdict on the same
document.
Validating payloads at runtime
This package does not bundle a JSON-Schema validator, so §7.2 items 1–2
(framework and payload schema validation) are yours to wire up. Fetch the
payload.schema.json from
https://trusttasks.org/spec/<slug>/<version> and feed it to
ajv or any other Draft 2020-12
validator.
Versioning
The package version follows Semantic Versioning over
this package's own API — the exported types, the runtime, and the generated
module surface. A breaking change to any of those bumps the leading non-zero
component (0.2.x → 0.3.0 while below 1.0); anything additive is a patch.
Behavioural changes count: when a specification starts declaring proof
REQUIRED, consumeInbound rejects documents it previously accepted, and the
version has to reflect that even though the wire format is unchanged.
The version is not tied to the SPEC.md
framework revision, which it once claimed to track. The two move for different
reasons — a framework revision can change the spec-authoring contract without
altering a single generated type. Read a document's framework version from the
specification's targetFrameworkVersion declaration instead.
A bump in an individual spec's own MAJOR.MINOR ships as a new exported module
name (AclGrant_v1_0 alongside AclGrant_v0_1) and so is additive here.
trust-tasks-rs and @openvtc/trust-tasks are usually released together and
carry matching version numbers, so a given pair is known to be generated from
the same registry revision. They diverge when a change is breaking on one side
only — 0.15.0 is such a release, hoisting the shared definitions in a way that
is safe here (TypeScript is structurally typed) and a coherence break in Rust.
One divergence is standing and worth knowing before you write a switch:
StandardCode is a closed union here and #[non_exhaustive] in
trust-tasks-rs, so a new SPEC §8.3 error code is a minor bump on this
package and a patch on the crate. Narrow with isStandardCode and keep a
default arm to be immune to it; the reasoning is on the type itself and in
CHANGELOG.md.
License
Apache-2.0. See LICENSE.md at the repo root.
Contributing
This package is generated, not authored by hand. To change the
exported surface, add or modify a spec under specs/ in the
dtgwg-trust-tasks-tf
repo and run npm run build-ts-bindings from the repo root. The
generator updates this package's src/ tree atomically. See
CONTRIBUTING-SPECS.md
for the spec authoring guide.
