Advanced and compatibility reference
Read this only for a task explicitly routed here by the main skill. This preserves existing lower-level capabilities; it is not the default new-application path. Check the installed declarations and the deployment’s operation support before using an older route. Never fall back from refused request-bound authorization to JWT/admin authorization. Retry advice here applies only when the operation is known safe to repeat; it never authorizes blindly repeating transaction submissions, native approval POSTs or signing requests with an uncertain outcome.
The committee path
Section titled “The committee path”The JWT path’s Session.decrypt reconstructs the master key in your process.
The committee path never does: per request, a verifier committee drawn by the
on-chain beacon co-signs one compound authorization token, and the keepers run
the sign or decrypt ceremony themselves, authorized, metered and audited at the
node. Endpoints come only from chain; there is no static fallback, and the flow
refuses to run without trustless VerifierSetRegistry inclusion proofs.
First check that the deployment accepts this flow. A keeper running the
production posture sets api.require_request_binding, which means a token is
only good for the one request whose hash it carries. The verifiers cannot bind
that hash — only the verifier agent sees the request — so a token gathered here
is refused by the keeper with HTTP 403 request binding required (api.require_request_binding) but token carries no request_hash — use a token issued by the Verifier Agent. There is no way around it from this side: on such
a deployment the authorization comes from the OID4VP flow in
tasra-oid4vp-wallet-and-verifier-agent, and everything below applies to
deployments that do not enforce binding. What is below still holds for reading
the slot, resolving keepers and verifiers, and encrypting, none of which needs a
token.
Then check the slot. The committee path reads the slot’s per-request verifier
policy, and a slot created without one has none: every call fails with
slot 0x… has no verifierPolicy (committee path not wired); use the JWT path. It is
set once, by the slot creator, with writer.setVerifierPolicy(slotId, committee, quorum)
(the verifier-policy section of tasra-create-slot/references/advanced.md) — not something this side can work around.
import {createHolderProof, fromBytes, type HolderSigner} from 'tasra-sdk'import {holderProofPerVerifier} from 'tasra-sdk/committee'import {createTasraChainClient, createCommitteeSlotClient, addressBookFromManifest, VERIFIER_TAG} from 'tasra-sdk/chain'import {ed25519ClientSigner} from 'tasra-sdk/committee'
// manifest is the checksum-verified download from tasra-releases.const chainId = manifest.chainIdconst chain = createTasraChainClient({rpcUrl, addresses: addressBookFromManifest(manifest), chainId})// The committee path needs KEY_REGISTRY (slot record + verifier policy), NODE_REGISTRY (keeper and// verifier URLs), THRESHOLD_BEACON (the per-request draw) and VERIFIER_SET_REGISTRY (the anchored// snapshot) in the book; a missing one throws on the read that needs it, not at construction.// verifierUrl below is one verifier from the on-chain directory — (await resolveVerifierDirectory(chain))[0].url// (tasra-chain) — or the single verifier of a small deployment.
const holder = 'did:key:z6Mk…' // the credentials' subject DIDconst credentials: string[] = [...] // compact-JWS verifiable credentialsconst slotId: `0x${string}` = '0x…' // bytes32 slot id, hexconst signer: HolderSigner = {alg: 'EdDSA', did: holder, secretKey} // secretKey: Uint8Array, the DID's 32-byte Ed25519 // authentication key; or {alg, did, sign(input)} for a callback/HSM// ONE PROOF PER VERIFIER. A proof names its audience and burns a nonce that lives in that one// verifier, so where the policy draws a committee the others answer 401 "aud does not include this// verifier". Pass the function and each drawn verifier gets a fresh proof, on every request:const holderProof = holderProofPerVerifier({signer, audience: 'your-verifier-iss', credentials, slotId})// A single string — await createHolderProof(verifierUrl, {signer, audience, credentials, slotId}) —// is enough only where the deployment has one verifier.const kk = createCommitteeSlotClient({ chain, holder, credentials, holderProof, clientSigner: ed25519ClientSigner(clientSecretKey), // optional, 32-byte Ed25519 secret; attributes the request bundle ttlSecs: 300, // compound-token TTL (default 300)})
// This is a BLS slot. Signing needs a separate FROST slot and authorization for that slot.const messageBytes = new TextEncoder().encode('hello')const env = await kk.encrypt(slotId, messageBytes, {identity: new TextEncoder().encode(slotId)}) // Uint8Array envelope; local, group key read from KeyRegistry
// decrypt takes the envelope's parts, not the bytes: split them with fromBytes —// ciphertext is an opaque Ciphertext structure, identity the label bytes (Uint8Array), epoch a bigintconst {ciphertext, identity, epoch} = fromBytes(env)const plaintext = await kk.decrypt(slotId, { // Uint8Array ciphertext, identity, ciphertextEpoch: epoch === null ? undefined : Number(epoch), decryptingSet, // number[]: k..n distinct BLS identifiers asked to run the ceremony blsPeers, // {id, peerId}[]: those keepers' libp2p peers — ceremony participants, not endpoints})encrypt needs no holder or credentials (it only reads the public group key), so
createCommitteeSlotClient({chain}) alone is a valid encrypt-only client, built
without any network call; sign and decrypt need holder, credentials and
holderProof.
decryptingSet and blsPeers are the one input you still supply: they name
which keepers run the decryption ceremony — k to n distinct BLS identifiers,
where k is (await chain.readers.keyRegistry.getKeySlot(slotId)).threshold.k.
Take them from the slot’s assigned keepers (resolveSlotKeeperUrls,
tasra-chain) and each keeper’s /v1/info (node_identifier → id,
peer_id → peerId); both fields are optional in NodeInfo, so skip a keeper
whose info lacks either. Those on-chain URLs are often in-cluster names, and
nodeApi.info needs them reachable from where you run.
VERIFIER_TAG is keccak256("verifier"), the NodeRegistry role tag the
verifier set is discovered by.
Before the first request, the real slot must have a nonzero verifier committee and quorum policy, a provisioned DCQL rule and funded usage. A BLS slot cannot perform FROST signing. For a separate signing slot, obtain a fresh holder proof bound to that slot; never reuse the encryption slot’s proof. Advertised keeper and verifier endpoints must be reachable from the application. This client does not rewrite internal cluster hostnames into public URLs.
Holder proofs and committees
Section titled “Holder proofs and committees”A holder proof is bound to a nonce that lives in one verifier’s store and is
consumed atomically: the verifier that validates it spends the nonce and refuses
that proof a second time, so one proof authorizes one request at one verifier.
A committee is several verifiers, so a single string cannot serve it — the ones
it does not name answer 401 holder proof rejected: … aud does not include this verifier, and there is no quorum.
holderProofPerVerifier({signer, audience, credentials, slotId}) is the answer
everywhere holder binding is enforced: it mints a fresh nonce-bound proof for
each drawn verifier, on every request. createCommitteeSlotClient takes it as
holderProof, and so do the lower-level flows (requestCommitteeToken,
committeeSign, committeeDecrypt) and the one-call committeeSignRequest,
committeeDecryptRequest and ibeDecryptRequest from tasra-sdk/committee.
A plain string still works where the deployment has a single verifier, and a
client built around one serves exactly one request.
tasra-sdk/committee
Section titled “tasra-sdk/committee”The internals: selectVerifierCommittee, compoundTokenHash,
assembleCompoundToken, verifyCompoundToken, Merkle proof helpers, wire
codecs, and the orchestration (committeeAuthorize, gatherCommitteeToken,
requestCommitteeToken, committeeSign, committeeDecrypt,
requestIbeExtractionPartials). Most consumers never import it;
createCommitteeSlotClient drives all of it. Reach for it when auditing or
re-implementing the protocol, or to verifyCompoundToken offline.
Common mistakes
Section titled “Common mistakes”- Passing
dcqlRulein the config. It is unused: the verifier fetches the slot’s rule from a keeper and hash-checks it. Remove it. - Passing node or verifier URLs. There is no way to; they come from chain.
- Running against a deployment without an anchored
VerifierSetRegistrysnapshot. The client throws rather than degrade; ask the operator. - One holder proof for several verifiers under
require_holder_binding, or for a second request. Its nonce is single-use: mint a fresh proof, or use the lower-level flow withholderProofPerVerifier. - Passing the envelope bytes to
decrypt. Split them withfromBytesfirst. - Passing a key string as
signer. It is aHolderSignerobject. - Choosing this path for convenience. Choose it when the key must never be reconstructed; otherwise the JWT path is simpler.
Where to read more
Section titled “Where to read more”node_modules/tasra-sdk/dist/chain/committeeClient.d.ts,node_modules/tasra-sdk/dist/committee/index.d.ts.
