@kblockai/sdk
v0.1.0-rc.1
Published
K-Block v0.1 TypeScript SDK: signed, content-addressed K-Objects, self-certifying identities (KIDs), delegations, encryption, workflows, portable receipts, relay transports and KBlockParticipant
Maintainers
Readme
@kblockai/sdk
Verify K-Block receipts offline, independently of K-Block. A receipt is a portable .kbundle file
that records a protected action: who requested it, who approved exactly which call, who executed it
and on whose behalf, all signed. This package checks every signature and every step itself.
The package also implements the K-Block v0.1 protocol for protocol participants: signed, content-addressed K-Objects, self-certifying identities (KIDs), scoped delegations, schema-driven workflows and portable receipts. K-Block is not a blockchain: there is no ledger, no consensus and no token.
Pre-release (0.1.0-rc.1). Most developers need this package only to verify receipts. Agents call
the K-Block Gateway with @kblockai/client (Node.js) or kblock (Python); neither depends on this
package.
Install
npm install --save-exact @kblockai/[email protected]- Node.js 22 or later (tested on Node.js 22.17), and current browsers (see "Browser support").
- ESM only; TypeScript declarations included. Runtime dependencies: only
@noble/curves,@noble/hashesand@noble/ciphers. - Entry points:
@kblockai/sdk/verify(receipt verification only),@kblockai/sdk(the whole protocol) and@kblockai/sdk/testing(test helpers, never for production). - TypeScript:
module/moduleResolutionnodenext(orbundlerwith a bundler); the subpaths need one of them. The declarations use the globalfetchandWebSockettypes: include theDOMlib, ortypes: ["node"]with@types/node22 or later.
Verify a receipt
verify_receipt.mjs, a complete program:
import { readFile } from "node:fs/promises";
import { renderText, verifyBundleJson } from "@kblockai/sdk/verify";
const [file, gatewayKid] = process.argv.slice(2);
if (!file) {
console.error("usage: node verify_receipt.mjs <receipt.kbundle> [<gateway KID>]");
process.exit(2);
}
const report = verifyBundleJson(await readFile(file, "utf8")); // never throws on invalid input
console.log(renderText(report)); // the seven checks, then RESULT: VALID or RESULT: INVALID
// VALID means the bundle is complete and consistent, and every object in it is signed by the keys its
// identity documents list. Whether YOUR Gateway issued it is a separate question: compare the issuer
// with the Gateway KID that K-Block gave you.
const fromGateway = gatewayKid === undefined || report.issuer === gatewayKid;
if (!fromGateway) console.log(`issuer ${report.issuer} is not the Gateway identity ${gatewayKid}`);
process.exit(report.valid && fromGateway ? 0 : 1);node verify_receipt.mjs receipt.kbundle "<your Gateway's KID>"renderText(report) returns the text report: the receipt and its issuer, what happened (outcome,
who requested, approved and executed the call, on whose behalf, the delegations, the result's
digest), the seven checks (Bundle format, Identity chains, Signature chain, Receipt coverage, Graph
integrity, Workflow & authorization, Receipt summary), warnings and errors, then RESULT: VALID or
RESULT: INVALID, and the notes. report.valid is true only when all seven checks pass and nothing
was found.
verifyBundleJson(text, options?)parses strictly and verifies;verifyBundle(value, options?)takes an already parsed value. Both return a report and never throw on invalid input.- The report:
valid,checks({ name, status: "VALID" | "INVALID" | "SKIPPED", detail? }),issuer,issued_at,outcome,summary(derived by the verifier itself),path,actors,attestations,errors,warnings,notes.report.toJSON()is identical to the JSON report of the K-Block reference implementation. KReceipt.verify(bundle)runs the same verification and flattens its verified summary (who did what):requestActor,proposer,approvedBy,executor,onBehalfOf,outcome,issuedAt.- Options:
trustedAttesters(KIDs whose attestations, such as "executed", you choose to trust),knownIdentities(identity documents you obtained independently, for key freshness),maxIdentityKeys(a budget for bundles from untrusted parties; beyond it the bundle is invalid withTOO_LARGE),workflows(the accepted workflow definitions; the built-ins by default) andverify.now(aTimestamp,toTimestamp(…), for reporting expired objects; it does not change validity). Kid.parseandparseObjectJson, for the KIDs and identity documents of your own configuration, throwProtocolErroron invalid input.
What a valid receipt proves
- Each covered object was signed by its actor, with a key that the verifier's identity view lists as valid at the object's asserted time, and was not altered after signing.
- The committed path from the result back to the request is exactly what was signed, with nothing unrelated attached.
- Each step was allowed by the named workflow (roles, guards, states, causal order).
- Who requested, who proposed, who approved exactly which proposal (by its content hash), who executed, and on whose behalf under which delegation chain.
- The delegation granted the action within its constraints and time window.
- The result's terms equal the approved terms, and the result payload has the stated digest.
In a receipt of a K-Block Gateway, the request, the proposal, the execution and the result are signed by the Gateway's identity on behalf of the organization (under the delegation the organization signed with its own key), and an approval by the approver's personal key; the agent itself signs nothing. Which agent and which API key made the call is the Gateway's own signed statement in the request.
What it does not prove
- That the real-world effect happened. A result is the executor's signed claim. Only an
attestation from an attester you trust (
trustedAttesters) adds evidence, and only as much as the attester is worth. - Which deployment issued it. A bundle signed with fresh keys is consistent with the identity
documents it carries. Compare
report.issuer(and the signers) with KIDs you know independently, such as your Gateway's KID, asverify_receipt.mjsdoes. - When things happened. Timestamps are asserted by the signers; only causal order is checked.
- That nothing conflicting exists outside the bundle. A bundle cannot show the absence of a
cancellation, a revocation, a newer identity document (unless you pass
knownIdentities) or a second consumer of the same approval. - That the issuer is honest or complete, or that self-asserted identity attributes (such as an actor's type) are true.
Security and trust model
- Verification trusts neither the transport nor the receipt's issuer, K-Block included: it re-verifies every signature, identity chain, delegation and workflow transition, derives the summary itself and compares it with the issuer's claims.
- Private keys stay local: a
LocalIdentitysigns in-process, and only public identity documents are published or embedded in bundles.exportSecrets()returns private keys: store them encrypted and never log them. For hardware-held keys, implement theSignerinterface. - Relays and transports are untrusted: they can withhold or delay objects, never forge, alter or re-attribute them. Every participant verifies what it receives.
- A service that acts on objects as they arrive must reject stale or future-dated objects
(
checkFreshness), transitions from expired parents (checkParentNotExpired) and replays (ReplayGuard), and should evaluate the whole thread, not just a path. - Disclosures placed in a bundle can be read by anyone who holds the bundle.
Protocol-level API
The package root exports the whole protocol. KBlockParticipant is a protocol participant over a
relay: it hides canonicalization, nonces, IDs and signatures, verifies everything it receives, and
checks every object it creates against the thread before publishing (preflight), so mistakes fail
fast with the error code a relay or verifier would report. From protocol_participant.mjs, a
complete example program:
import { KBlockParticipant, KReceipt, LocalIdentity, MemoryRelay, ProtocolError } from "@kblockai/sdk";
const relay = new MemoryRelay(); // in-process; RelayClient talks to a K-Block relay over HTTP
const alice = new KBlockParticipant({ identity: LocalIdentity.generate("human"), transport: relay });
const bot = new KBlockParticipant({ identity: LocalIdentity.generate("agent"), transport: relay });
await alice.register(); // publishes identity documents (public keys only)
await bot.register();
// Alice gives the agent scoped, time-limited authority, then a task.
const delegation = await alice.delegations.create({
delegate: bot.kid,
permissions: ["hotel.search", "hotel.book"],
constraints: { maxAmount: { amount: "300", currency: "USD" } },
expiresIn: 24 * 3600,
});
const task = await alice.tasks.create({
assignedTo: bot.kid,
payload: { instruction: "Find me a hotel under $300", budget: { amount: "300", currency: "USD" } },
});
// The agent proposes exact terms.
const terms = { hotel: "Hotel X", check_in: "2026-10-01", nights: 1, price: { amount: "250", currency: "USD" } };
const proposal = await bot.proposals.create({ task, summary: "Hotel X, 1 night, 250 USD", terms });
// Every participant checks what it creates against the thread before publishing it: the agent cannot
// approve its own proposal, so the SDK refuses locally with the protocol's error code.
try {
await bot.approvals.approve(proposal);
} catch (e) {
if (e instanceof ProtocolError) console.log("refused:", e.code); // "UNAUTHORIZED_ACTOR"
else throw e;
}
// Alice approves that proposal (bound by its content hash).
const approval = await alice.approvals.approve(proposal, { note: "Book it." });
// The agent executes and records the result on Alice's behalf, under the delegation.
const execution = await bot.executions.create({ parent: approval, action: "hotel.book", delegation: delegation.id });
const result = await bot.results.create({ parent: execution, delegation: delegation.id, output: { confirmation: "HX-4471" } });
// A portable receipt: it verifies offline, without trusting the relay or the issuer.
const bundle = await alice.receipts.issue(task.id, result.id);
const v = KReceipt.verify(bundle);
console.log("receipt:", v.valid, v.outcome); // true "succeeded"
console.log("approved by alice:", v.approvedBy === alice.kid, "executed by bot:", v.executor === bot.kid, "on behalf of alice:", v.onBehalfOf === alice.kid);Where a method takes a task, proposal, approval or execution, you can pass the object or its ID (an
ID is fetched and verified). Recipients come from the workflow roles, terms are copied from the
approved proposal, and onBehalfOf is the principal at the top of the delegation chain.
Receiving: participant.subscribe(handler) delivers verified objects from a relay over WebSocket
(backlog first, duplicates dropped, at-least-once, acknowledged after the handler resolves);
participant.inbox.list() and inbox.ack() poll; participant.threads.get(id) returns a locally
verified view of a whole thread.
Errors are ProtocolError (with isProtocolError(e, code?)): a protocol or verification failure,
with the reference implementation's codes, such as ID_MISMATCH, INVALID_SIGNATURE and
UNAUTHORIZED_ACTOR. A relay that refuses a request or cannot be reached gives RelayError (with
.status); errorCode(e) reads the code from either. ProtocolError is unrelated to
@kblockai/client's KBlockProtocolError, which means a Gateway answer that is not a K-Block
answer.
Transports: RelayClient talks to a K-Block relay over fetch and WebSocket (both injectable),
signing its requests and refusing answers that do not match them; MemoryRelay is an in-process
relay with the same checks, for tests and demos.
Building blocks:
| Area | API |
|---|---|
| Objects | ObjectBuilder, parseObject / parseObjectJson, verifyObject, canonicalize (KB-JCS) |
| Identities | LocalIdentity, IdentityChain, IdentityRegistry |
| Delegations | verifyAuthority, checkConstraints, permissionMatches |
| Encryption | encrypt, decrypt, verifyDisclosure (X25519, HKDF-SHA256, ChaCha20-Poly1305) |
| Workflows | WorkflowRegistry, ObjectSet, verifyGraph |
| Live receivers | checkFreshness, checkParentNotExpired, ReplayGuard |
| Receipts | issueReceipt, verifyBundle, bundleToJson |
| Relay authentication | AuthFields, authenticate, SignatureCache |
Encryption: let ObjectBuilder draw a fresh encryption seed. A caller-supplied seed exists only for
deterministic test vectors; reusing a seed breaks confidentiality.
Browser support
The library imports no Node.js modules. On Node.js, when process.getBuiltinModule passes a
self-test, Ed25519 signing, the group equation of verification and SHA-256 of large inputs use
node:crypto; every strictness check stays in JavaScript, and browsers use @noble only. Otherwise
it relies on crypto.getRandomValues, and on fetch and WebSocket for RelayClient only.
The package ships ES2022 JavaScript: a bundler must target ES2022 or transpile it. A browser bundle of
@kblockai/sdk/verify was run in a current Chromium with no polyfill. @noble calls Object.hasOwn
while it loads, so engines older than Chrome 93, Firefox 92 and Safari 15.4 need a polyfill for it.
Testing helpers
@kblockai/sdk/testing exports DemoActors, runHotelDemo and DEMO_WORKFLOW, which run the
protocol's example workflow end to end. Never use them in production:
DemoActors.deterministic builds identities from secret keys that are public.
Versioning
0.1.0-rc.1 is a pre-release. Package versions follow semver; VERSION ("0.1") is the protocol
version and is independent of the package version.
License
K-Block SDK License (proprietary); see the LICENSE file in this package.
