trust_tasks_https 0.1.0
trust_tasks_https: ^0.1.0 copied to clipboard
The Trust Tasks HTTPS transport binding for Dart: a typed client and a framework-agnostic server running the SPEC section 7.2 pipeline per request.
trust_tasks_https #
The Trust Tasks HTTPS transport binding
(bindings/https/0.2) for Dart: a
typed client, and a server that runs the SPEC §7.2 consumer pipeline on every
request. Built on package:trust_tasks;
the Dart counterpart of the Rust
trust-tasks-https crate, and
interoperable with it in both directions.
dart pub add trust_tasks trust_tasks_https
Requires Dart 3.3 or later.
Client #
import 'package:trust_tasks_https/trust_tasks_https.dart';
final client = HttpsClient(
base: Uri.parse('https://maintainer.example'), // POSTs to <base>/trust-tasks
serverVid: 'did:web:maintainer.example',
myVid: 'did:web:org.example',
token: 's3cret',
);
final response = await client.send<acl_grant.Payload, acl_grant.Response>(
request,
encode: (p) => p.toJson(),
decodeResponse: acl_grant.Response.fromJson,
);
send fills in issuer, recipient and issuedAt where the request leaves
them unset. It returns the #response document only when that document belongs
to this request: same thread, the request's type with #response, issued by
serverVid, addressed to myVid. Anything else throws a subclass of the sealed
HttpsClientException:
| Exception | Meaning |
|---|---|
TrustTaskErrorException |
The server refused with a trust-task-error; the code is authoritative |
DuplicateAbsorbedException |
202/204: the server had already accepted this document. Not a failure |
HttpStatusException |
A non-2xx with no error document — a proxy, a 401. Says nothing about the task |
TransportException |
No answer at all. Retry by re-sending the same bytes (SPEC §8.4) |
ResponseMismatchException |
A response, but not to this request, or not from this server |
ResponseDecodeException, ResponseProofException |
A response this client cannot accept |
Use sendAck for a specification that defines no success response. Pass
responseVerifier to require a verified proof on every response. The client
uses package:http, so it runs on the web and in Flutter.
Server #
import 'package:trust_tasks_https/io.dart';
final server = HttpsServer(
localVid: 'did:web:maintainer.example',
auth: StaticBearerAuth({'s3cret': 'did:web:org.example'}),
proofVerifier: DataIntegrityProofVerifier.forDidKey(), // trust_tasks_proof
)
..on<acl_grant.Payload, acl_grant.Response>(
spec: acl_grant.spec,
decode: acl_grant.Payload.fromJson,
encode: (p) => p.toJson(),
encodeResponse: (r) => r.toJson(),
handler: (doc, ctx) async {
if (!mayGrant(ctx.resolved.issuer)) {
throw ctx.refuse(const RejectReason(
code: StandardCode.permissionDenied,
message: 'not an administrator',
));
}
return acl_grant.Response(entry: await grant(doc.payload.entry));
},
)
..enableDiscovery();
await serve(server, port: 8443, securityContext: tls);
HttpsServer.handle takes a plain HttpsRequest and returns an HttpsReply,
so the server is not tied to dart:io: package:trust_tasks_https/io.dart
adapts it to dart:io, and shelf, dart_frog or a Cloud Functions handler need
only a few lines. io.dart is a separate library so that the client keeps
running on the web.
What every request goes through #
In the same order as the Rust server, so that the two refuse the same document
for the same reason, and so that nothing an unauthenticated sender picks —
above all resolving a did:web during proof verification — runs before the
cheap local checks:
POST <base>/trust-tasks,Content-Type: application/json, at most 256 KiB.- The bearer token is mapped to the sender's VID.
- An unregistered
typeisunsupportedType. - The payload decodes; the in-band identity agrees with the token (§4.8.1); expiry, recipient and the freshness window hold.
- Attribution: a request with neither a recognised token nor a
proofis refused. §5 of the binding does not let a proof be omitted, and without this anyone could claim anyissuer. allowedDidMethods, if set, screensproof.verificationMethod.- The proof verifies — or, with no
proofVerifier, a proof is refused rather than silently ignored. - The specification's own policy, then the duplicate-execution claim: a
byte-identical resend gets the first response back without running the
handler again, a different document under the same
idisidConflict, and a store that cannot be consulted fails closed withunavailable.
Statuses follow the binding's §4 table. The codes that concern identity —
proofRequired, proofInvalid, identityMismatch, wrongRecipient — all
share 422, and a contested identity with no authenticated sender is answered
exactly like an unparseable body, so none of them works as an oracle.
Behind a load balancer, pass a shared replayGuard. The default record is
in-process; two replicas would each accept the same document once (§5.1).
Interoperability #
Checked against the Rust crate's own demos, with trust_tasks_proof signing
and verifying:
- a Dart client sending a signed
acl/grantto the Rustserver_demogets the response; the retry gets the same response without the handler running again, and a document whose signature no longer matches is refused withproofInvalid; - the Rust
client_demosending a signedacl/grantto a Dart server gets the response, including payload members the Dart types model only as optional.
License #
Apache-2.0.