Docs

Receipt verification SDK

Verify a K-Block receipt (.kbundle) offline in Node.js or the browser with @kblockai/sdk/verify, read the report and pin your Gateway's KID.

A receipt is a portable .kbundle file that records a protected action: who requested it, who approved exactly which call, who executed it (for a Gateway-enforced call) and on whose behalf, all signed. @kblockai/sdk/verify checks such a file offline: it re-verifies every signature, identity chain, delegation and workflow step itself and derives the summary on its own. It trusts neither the transport nor the issuer, K-Block included, and needs no network access and no K-Block account.

Use it to check receipts in your own systems: an audit pipeline, a support tool, a customer-facing page. The dashboard's Verify in this browser button runs the same verifier.

Install

npm install --save-exact @kblockai/sdk@0.1.0-rc.1
RequirementValue
Node.js22 or later; also current browsers through a bundler (see Verify in the browser)
Module formatESM only, ES2022 JavaScript, TypeScript declarations included
Runtime dependencies@noble/curves, @noble/hashes, @noble/ciphers only
TypeScriptmoduleResolution nodenext or bundler, so that the subpath entry points resolve

The package has three entry points:

Entry pointFor
@kblockai/sdk/verifyReceipt verification only. Use this one.
@kblockai/sdkThe whole K-Block v0.1 protocol implementation, for protocol participants. It also exports everything in /verify, as the same functions and classes.
@kblockai/sdk/testingTest and demonstration helpers only. Never use it in production (see Testing helpers).

Agents do not need this package: @kblockai/client and the Python package call the Gateway without it.

Get a receipt file

  • In the dashboard: open Receipts, select a receipt, then Download .kbundle.
  • From the Cloud API, at the base URL that K-Block gives you: GET /api/v1/orgs/<organization-id>/receipts/<receipt-id>/bundle returns the file (application/vnd.kblock.bundle+json). It needs a signed-in member of the organization (any role) or a management API key of the organization with the receipts:read scope, sent as Authorization: Bearer kb_live_…. The agent's gateway key cannot read receipts. client.receiptUrls(result) in the TypeScript client builds this URL from a call's result.

The receipt ID of a call is in its result (receiptId in TypeScript, receipt_id in Python).

Verify in Node.js

This complete program verifies a file, prints the text report, and checks that your Gateway issued it:

verify-receipt.ts
import { readFile } from "node:fs/promises";
import { renderText, verifyBundleJson } from "@kblockai/sdk/verify";

const [file, gatewayKid] = process.argv.slice(2);
if (file === undefined || gatewayKid === undefined) {
  console.error("usage: verify-receipt <receipt.kbundle> <your Gateway's KID>");
  process.exit(2);
}

// Never throws on an invalid bundle: every finding is in the report.
const report = verifyBundleJson(await readFile(file, "utf8"));
console.log(renderText(report)); // the seven checks, then RESULT: VALID or RESULT: INVALID

// VALID means the bundle is complete, consistent and signed by the keys its identity documents list.
// Whether YOUR Gateway issued it is a separate check: compare the signers with a KID you know.
const GATEWAY_STEPS = new Set(["task", "proposal", "execution", "result"]);
const issuedByGateway =
  report.issuer === gatewayKid &&
  report.path[0]?.type === "task" &&
  report.path.every((p) => !GATEWAY_STEPS.has(p.type) || p.actor === gatewayKid);
if (!issuedByGateway) console.log(`not issued by the Gateway ${gatewayKid} (issuer: ${report.issuer ?? "none"})`);

process.exit(report.valid && issuedByGateway ? 0 : 1);

verifyBundleJson(text, options?) parses the text strictly and verifies it. verifyBundle(value, options?) does the same for a value you already parsed. Both return a ReceiptReport and report every problem instead of throwing; the only exception is a TypeError for an invalid maxIdentityKeys option.

The report

report.valid is true only when there are no errors and all seven checks are VALID.

CheckWhat it covers
Bundle formatThe container and the receipt payload parse under the strict rules, within the size limits.
Identity chainsEach signer's identity documents form a valid chain.
Signature chainEvery object is signed by its actor with a key valid at that point, and its ID matches its content.
Receipt coverageThe receipt covers exactly the signed path and pins the identity documents of all signers.
Graph integrityEvery reference resolves; nothing is missing or from another thread.
Workflow & authorizationEach step is allowed by the named workflow (roles, guards, states), within the delegation.
Receipt summaryThe issuer's summary equals the summary the verifier derives itself.

Each entry of report.checks is { name, status, detail? } with status VALID, INVALID or SKIPPED. A check is SKIPPED when an earlier problem stopped verification before it ran.

FieldMeaning
receipt, issuer, issued_atThe receipt object's ID, the KID that signed it, and its asserted creation time.
workflow, workflow_digest, outcomeThe workflow the thread follows and the outcome (for example the result status; pending or granted for a decision-only authorization receipt, which proves the authorization, not an execution).
summaryWho did what, derived by the verifier (never copied from the issuer): for example requested_by, approved_by, executed_by, executed_on_behalf_of, result_digest.
pathThe signed steps from the request to the result: { id, type, actor, on_behalf_of?, created_at, state? }.
actorsEach signer's KID, actor type and the identity document the receipt pins.
attestationsAttestations in the bundle, with trusted set only for attesters you listed in trustedAttesters.
errors, warningsFindings as { code, object?, message }. Errors make the report invalid; warnings do not.
notesWhat this verification could not establish, for example that key freshness was not checked.

renderText(report) returns the human-readable report: the receipt and its issuer, what happened, the seven checks, warnings and errors, RESULT: VALID or RESULT: INVALID, and the notes. Values from the bundle are escaped before printing, so a bundle cannot fake a result line on your terminal. report.toJSON() returns the report as plain JSON for storage or comparison.

For "who did what" alone, KReceipt.verify(bundle) runs the same verification and flattens the summary:

who-did-what.ts
import { readFile } from "node:fs/promises";
import { KReceipt } from "@kblockai/sdk/verify";

const v = KReceipt.verify(await readFile(process.argv[2] ?? "receipt.kbundle", "utf8"));
console.log(v.valid, v.outcome, v.issuedAt);
console.log({ requestedBy: v.requestActor, proposedBy: v.proposer, approvedBy: v.approvedBy, executedBy: v.executor, onBehalfOf: v.onBehalfOf });
console.log(v.resultHash); // sha256:<hex> of the result payload (commitment:… when it is encrypted)

Pin your Gateway's KID

A bundle signed end to end by freshly generated keys is internally consistent, so it verifies. VALID alone does not tell you that your Gateway issued it. To establish that, compare the signers with your Gateway's KID (its self-certifying identity, kid:kb:…; see Identities and KIDs):

  • report.issuer must be the Gateway's KID.
  • report.path must start with a task.
  • In a K-Block Gateway receipt, the Gateway signs the request (task), the proposal, the execution and the result (all but the proposal on behalf of your organization), and an approver signs an approval or a rejection with their own key. The agent signs nothing. So every task, proposal, execution and result step in report.path must have the Gateway's KID as actor. The dashboard applies the same three rules.

Take the KID from a source independent of the bundle and of the Cloud API that served it, and keep it in your own configuration: use the Gateway KID that K-Block gave you, or the one the dashboard shows right after you create a gateway API key. If the dashboard does not show it, contact K-Block support. Do not read it from the bundle you are checking.

To also detect a key-compromise report or a newer identity document that the bundle leaves out, pass the Gateway's current identity documents as knownIdentities. Identity documents are self-certifying (a KID is derived from the identity's first key), and documents that are not part of the signer's authenticated chain are ignored, so you can take them from the Cloud API: GET /api/v1/gateway/identity (any signed-in user or any management API key, whatever its scopes) returns { kid, documents }, or 404 when K-Block has no Gateway identity registered yet, a problem on K-Block's side (contact K-Block support with the request ID from the response's X-Request-Id header). The KID you compare against must still come from your own configuration. Without knownIdentities, notes says that key freshness was not checked for those signers; the report can still be valid.

The program below reads the Gateway KID from KBLOCK_GATEWAY_KID, an environment variable of your own, and the Gateway's identity documents from gateway-identity.json, the documents array saved to a file.

pinned.ts
import { readFile } from "node:fs/promises";
import { Kid, parseObjectJson, verifyBundleJson } from "@kblockai/sdk/verify";

// Your configuration: the Gateway's KID, and its identity documents
// (the `documents` array of GET /api/v1/gateway/identity, saved to a file).
const gatewayKid = Kid.parse(process.env.KBLOCK_GATEWAY_KID ?? ""); // throws ProtocolError for a malformed KID
const documents = JSON.parse(await readFile("gateway-identity.json", "utf8")) as unknown[];

const report = verifyBundleJson(await readFile("receipt.kbundle", "utf8"), {
  knownIdentities: documents.map((d) => parseObjectJson(JSON.stringify(d))),
});
// Apply all three rules above; this example shows the issuer check only.
console.log("valid:", report.valid, "issuer is our Gateway:", report.issuer === gatewayKid);
console.log(report.notes); // the key-freshness note no longer counts the Gateway (an approver may remain)

Verify in the browser

The verifier imports no Node.js modules: in a browser all cryptography runs in @noble. On Node.js it uses node:crypto to speed up some operations when available; every check stays the same. Bundle it with any bundler that targets ES2022 or transpiles to your targets.

verify-in-browser.ts
import { verifyBundleJson } from "@kblockai/sdk/verify";

// <input type="file" id="kbundle" accept=".kbundle"> and <pre id="result"></pre> on your page.
const input = document.querySelector<HTMLInputElement>("#kbundle");
const output = document.querySelector<HTMLPreElement>("#result");

input?.addEventListener("change", async () => {
  const file = input.files?.[0];
  if (file === undefined || output === null) return;
  const report = verifyBundleJson(await file.text()); // runs entirely in the browser
  output.textContent = [
    `RESULT: ${report.valid ? "VALID" : "INVALID"}`,
    `issuer: ${report.issuer ?? "none"}`,
    ...report.checks.map((c) => `${c.name}: ${c.status}${c.detail === undefined ? "" : ` (${c.detail})`}`),
    ...report.errors.map((e) => `error ${e.code}: ${e.message}`),
  ].join("\n");
});

When you display values from a bundle in your own HTML, set them as text (as above), never as HTML: a bundle is untrusted input.

Options

OptionDefaultDescription
trustedAttestersnoneKIDs whose attestations (for example "executed") you choose to trust. Only these set trusted in report.attestations.
knownIdentitiesnoneIdentity documents you obtained independently (parseObjectJson), for key freshness.
maxIdentityKeysunlimitedThe most public keys the bundle's identity documents may list in total. Set it when you verify bundles from untrusted parties: beyond it the bundle is refused with TOO_LARGE before any key is checked.
workflowsthe built-in workflowsA WorkflowRegistry of accepted workflow definitions.
verify.nownoneA Timestamp (toTimestamp(…)) used to report expired open objects. It does not change validity.
verify.skewMs300000 (5 minutes)The clock skew tolerated between causally related objects, in milliseconds.

A bundle may hold at most 1024 objects, 1024 identity documents and 1024 disclosures (MAX_BUNDLE_OBJECTS, MAX_BUNDLE_IDENTITY_DOCS, MAX_BUNDLE_DISCLOSURES); larger bundles are invalid.

Errors

  • verifyBundleJson, verifyBundle and KReceipt.verify never throw for a bad bundle; read report.valid, report.checks and report.errors. Error codes are the protocol's (for example INVALID_SIGNATURE, UNKNOWN_KEY, MISSING_REFERENCE, UNAUTHORIZED_ACTOR); see Receipt verification.
  • Kid.parse and parseObjectJson, which you use for your own configuration, throw ProtocolError on invalid input. isProtocolError(e, code?) tests for it.
  • ProtocolError is unrelated to the TypeScript client's KBlockProtocolError, which means a Gateway answer that is not a K-Block answer.

What VALID does not prove

A valid receipt proves that each covered object was signed by its actor and not altered, that the steps follow the workflow and the delegation, and that the result's terms equal the approved terms. 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 adds evidence), which Gateway issued it (pin the KID, above), when things happened (timestamps are asserted by the signers; only causal order is checked), or that nothing conflicting exists outside the bundle. Read What a receipt proves before you rely on a receipt.

Testing helpers are not for production

@kblockai/sdk/testing exports DemoActors, runHotelDemo and DEMO_WORKFLOW, which run the protocol's example workflow end to end for tests and demonstrations. DemoActors.deterministic builds identities from secret keys that are public, so anyone can sign as them. They are kept out of the package root on purpose. Never use this entry point in production code, and never trust a receipt signed by its actors.

Next steps