
Company News
AWS Security Hub Adds Socket for Supply Chain Security
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.
@usekaval/kaval
Advanced tools
Before an AI agent acts, Kaval verifies the facts the action depends on and returns ALLOW, REVIEW, or BLOCK with an Ed25519-signed receipt.
Before an AI agent acts, send Kaval the action. Kaval identifies the facts that action depends on,
checks them against the sources it watches, and answers ALLOW, REVIEW, or BLOCK with a signed
receipt.
Policy engines decide whether an action is permitted under the rules; Kaval verifies whether the facts those rules depend on are still true.
npm install @usekaval/kaval
0.6 is a breaking release.
audit,gate,verifyBelief,extractAndCheck,scanStore,monitor,kaval, andkavalBatchare gone — they all collapsed intocheck(). The old routes answer410 tool_retired, which this client raises asKavalRetiredError. See Migrating from 0.5.
This package is ESM-first ("type": "module"). Use import in ESM projects, or dynamic import
in CommonJS:
const { Kaval } = await import("@usekaval/kaval");
CJS require("@usekaval/kaval") needs Node ≥20.19 or ≥22.12 (Node’s native
require(esm) support). On Node 18, use import / await import() instead — engines.node is
>=18 for ESM + fetch, not for CJS require.
Three entry points, one zero-dependency package:
| Import | What it is |
|---|---|
@usekaval/kaval | the API client — check() and everything that configures it |
@usekaval/kaval/verify | the offline verifier — receipts + webhook signatures, no network code |
@usekaval/kaval/verify/discovery | live HTTPS key discovery, kept separate so the network choice is loud |
It also installs one command, kaval-receipt-verify. See
/verify.
import { Kaval } from "@usekaval/kaval";
const kaval = new Kaval({ apiKey: process.env.KAVAL_API_KEY });
const result = await kaval.check({
action: "Issue Acme a $12,000 refund",
context: "billing record acme-2026 says the contract allows it",
materiality: "critical",
});
if (result.decision !== "ALLOW") {
// REVIEW is never permission to act.
const moved = result.facts.filter((fact) => fact.status !== "holds");
throw new Error(`blocked on: ${moved.map((f) => f.text).join("; ")}`);
}
Already know which facts matter? Name them and skip extraction entirely — structured claims canonicalize straight to a fingerprint, so there is no model call at all on the compile step:
const result = await kaval.check({
claims: [
{
subject: "Acme",
predicate: "refund_eligibility_window_days",
object: 90,
scope: { contract: "acme-2026" },
materiality: "critical",
},
"Acme's vendor security attestation is unexpired",
],
mode: "fast",
});
result.decision; // "ALLOW" | "REVIEW" | "BLOCK"
result.reason_codes; // ["ALL_FACTS_HOLD"] | ["FACT_CHANGED"] | … (a closed set of 8)
result.facts; // [{ fingerprint, text, status, materiality, served_from_state,
// last_verified_at, sources: [{ locator, version_sha256, fetched_at }] }]
result.receipt; // { id, signature, signed_at }
result.latency_ms; // { compile, lookup, live, total }
unknown, mid-re-evaluation, or changed at low/medium materiality.
Never permission to act.changed, or a critical fact is unknown.Per-fact status is holds | changed | unknown, so you can see exactly which belief moved
rather than just that something did.
| option | meaning |
|---|---|
action | what you are about to do, in plain language (required unless claims is given) |
context | what you already believe that bears on it — the retrieved chunk, the cached field |
claims | check these facts directly: plain sentences or {subject, predicate, object, scope} (max 20) |
mode | "standard" (default, may research) or "fast" (stored state only) |
max_wait_ms | live-research budget, default 100000, max 100000; 0 disables research (what mode: "fast" sets) |
origin_urls | authoritative sources for this action (max 20), merged with what the workspace watches |
materiality | low | medium | high | critical |
as_of | RFC 3339 cutoff for what the action may rely on |
The default is "let the research finish": a cold check has to search, fetch and adjudicate several
novel facts, and that routinely takes 50–100s. Lower max_wait_ms only when you would rather have a
bounded REVIEW than an answer — a fact that misses the budget comes back unknown. Do not count
on the detached remainder warming the next call: the next check recompiles the action and asks about
different fingerprints. What makes a check warm is a watched source (below), and warm checks are
a database read: ~50ms, zero model calls, zero fetches.
The three numbers are exported, so you can bound your own inputs against the server's:
import {
MIN_CHECK_MAX_WAIT_MS, // 0 — disables research, same as mode: "fast"
DEFAULT_CHECK_MAX_WAIT_MS, // 100000 — what the server applies when you omit it
MAX_CHECK_MAX_WAIT_MS, // 100000 — equal to the default; you can only ask for less
} from "@usekaval/kaval";
const receipt = await kaval.getReceipt(result.receipt.id);
Returned exactly as signed. Because the decision table is published, the receipt's own fact list
re-derives the verdict offline, byte for byte, with no server — verify the Ed25519 signature with
@usekaval/kaval/verify, which ships inside this package.
Each fact carries its basis: the sources it was proved against. When a basis entry has a
version_sha256, it also names what that digest covers — version_sha256_of: "canonical_text" (the
extracted text, with parser_name / parser_version naming the extractor) or "raw_bytes" (the
document as fetched). A PDF has both and they differ, so re-hash the artifact the label names. All
three labels are inside the signed bytes, so a rewritten one fails verification.
/verify — the offline verifierimport { verifyReceipt, verifyWebhookSignature } from "@usekaval/kaval/verify";
Everything Kaval signs, checked without Kaval: the Ed25519 receipt a check returns, and the HMAC
webhook signature on an inbound fact_state.delta (worked example).
The subpath is the whole verifier: zero dependencies, and nothing in its import graph touches the
network — no fetch, no node:http, no sockets, transitively. That is enforced by a test that
walks the real module graph of both the source and the shipped dist, so "offline" is a checked
property rather than a promise. It never reads an API key and never contacts Kaval, which is what
makes it something you can hand to a counterparty who does not trust us.
verifyReceipt answers three questions separately, and conflating them is the mistake it exists
to prevent:
key_id active or benignly retired, or revoked/compromised?fresh, recheck_due, expired, or
not_yet_issued?A valid signature proves who sealed these exact bytes. It does not prove the claim is true, that its
evidence is still current, or that the key is still trusted. accepted means signature and key
trust; freshness is reported alongside and never gates it.
import {
extractReceipt,
parseJsonStrict,
verifyReceipt,
} from "@usekaval/kaval/verify";
// Accepts a bare receipt, `{ packet }`, or the `{ run: { packet } }` share wrapper.
const receipt = extractReceipt(parseJsonStrict(receiptText));
const keyset = parseJsonStrict(keysetText);
const result = verifyReceipt(receipt, keyset, { at: "2026-07-20T12:00:00.000Z" });
result.cryptographic.valid; // the bytes really were signed by this key
result.key.lifecycle_status; // "active" | "retired" | "revoked" | "compromised" | "unknown"
result.key.trusted;
result.freshness.status; // "fresh" | "recheck_due" | "expired" | "not_yet_issued" | "unknown"
result.accepted; // cryptographic.valid && key.trusted
Use parseJsonStrict (or verifyReceiptText, which does it for you) on anything that arrives as
untrusted JSON text. Plain JSON.parse silently discards duplicate-member and lossy-number
evidence before any object-level verifier can see it.
Exported: verifyReceipt · verifyReceiptText · extractReceipt · verifyWebhookSignature ·
parseJsonStrict · stableCanonicalJson · canonicalUnsignedReceiptJson ·
canonicalUnsignedReceiptBytes · parseVerificationKey · verificationKeyFromDocument ·
isRfc3339Timestamp · parseRfc3339Instant · rfc3339TimestampMilliseconds ·
rfc3339TimestampNanoseconds · KAVAL_CANONICALIZATION · MAX_JSON_NUMBER_CHARACTERS ·
WEBHOOK_SIGNATURE_VERSION · WEBHOOK_SIGNED_CONTENT · DEFAULT_WEBHOOK_TOLERANCE_SECONDS.
Both documents Kaval signs verify here: a ProofPacket, whose signature block is
{algorithm, key_id, signature}, and a /v1/check receipt, which adds signed_at. That block is a
closed allowlist — those four members and nothing else — so an appended field fails closed instead of
shadowing the algorithm or key a lax verifier reads. Nothing inside the block is covered by the
signature (canonicalization strips the whole block before hashing), so signed_at is authenticated
indirectly, by being required to equal the signed checked_at. A check receipt carries no expiry,
so its freshness is honestly unknown.
verifyReceipt takes a key document you already hold — archive the keyset next to the receipt and
the verification is reproducible forever. If you would rather fetch it live, that is a separate
subpath, precisely so choosing the network is explicit:
import { discoverVerificationKeyDocument } from "@usekaval/kaval/verify/discovery";
const keys = await discoverVerificationKeyDocument(
"https://api.usekaval.com/v1/proof-verification-keys",
keyId,
);
Discovery is bounded to 5s and 256 KiB, requires HTTPS, refuses redirects and URL credentials, and
allows plain HTTP only for loopback when you pass allow_http_loopback.
kaval-receipt-verifyInstalling this package installs the verifier as a command, so a counterparty can check a receipt without writing any code:
npx --package @usekaval/kaval kaval-receipt-verify verify receipt.json --keyset keys.json
Usage:
kaval-receipt-verify verify <receipt.json|-> --keyset <keys.json> [options]
kaval-receipt-verify verify <receipt.json|-> --key-url <https-url> [options]
Options:
--keyset <path> Offline per-key document or keyset (recommended for reproducibility)
--key-url <url> HTTPS per-key endpoint or keyset endpoint
--at <RFC3339> Evaluate freshness at an explicit time
--require-fresh Exit non-zero unless freshness is "fresh"
--allow-http-loopback Permit http://localhost/127.0.0.0/8/::1 for local development
--compact Emit compact JSON
-h, --help Show this help
It prints the full VerificationResult as JSON and takes the receipt from a file or - (stdin).
Exit 0 means the signature is valid and the key is trusted — an expired receipt still exits
0, because freshness is a different question; add --require-fresh to make anything but fresh
exit non-zero. Exit 1 is a completed verification that was not accepted. Exit 2 is an
input, I/O, or discovery failure.
--at and every receipt/key timestamp must be a component-valid RFC 3339 instant. Date-only
strings, impossible calendar days (2026-02-29), leap-second :60, and the -00:00
unknown-local-offset marker fail closed rather than being normalized by Date.parse, and freshness
comparisons keep all nine fractional-second digits as exact epoch nanoseconds.
retired is benign rotation: historical signatures still verify.revoked and compromised stay mathematically checkable but never yield key.trusted: true — a
compromised signer can backdate its own issued_at, so no self-asserted timestamp rescues them.// Registering the NAME of an authority is usually enough.
const { source, resolved, authority } = await kaval.addSource({
kind: "entity",
name: "Aetna",
intent: "payer policy bulletins",
});
await kaval.listSources(); // includes sources auto-registered by a check
await kaval.listSources({ includeInactive: true });
await kaval.pauseSource(source.id); // stop polling without forgetting it
await kaval.resumeSource(source.id);
await kaval.deleteSource(source.id);
await kaval.getSource(source.id);
await kaval.recompileSource(source.id); // re-derive how Kaval fetches and parses it
kind: url (one page) · entity (a name to resolve) · push (a document you send in) ·
connection (a configured system of record). Kaval polls adaptively — slower when nothing changes,
faster when it does — and re-evaluates the facts that depend on a source when it moves.
authority is the resolver's working — one {url, outcome, reason} per candidate it considered.
accepted and discarded are informational; ambiguous is the one to act on: a real page of
the real entity, governing a different product line with different rules. Kaval refuses to guess
which of those you meant, so add or reject them yourself:
for (const decision of authority ?? []) {
if (decision.outcome === "ambiguous") {
console.warn(`ambiguous: ${decision.url} — ${decision.reason}`);
}
}
discovery_error is set when the source registered but Kaval could not derive an acquisition plan
for it. recompileSource(id) re-runs that derivation — it is the recovery path from a broken plan,
and the way a directly-registered kind: "url" source gets a plan at all. It answers 202 with a
job_id because discovery runs on the worker, not in your request.
fact_state.delta webhooksWatching only helps you if you hear about it. Subscribe once, at deploy time:
const { subscription, webhook_verification } = await kaval.subscribeFactStateDeltas({
callback_url: "https://your-app.example.com/hooks/kaval",
external_scope_ids: ["contract:acme-2026"], // optional scope filter
});
// webhook_verification.secret is shown EXACTLY ONCE — store it; it is how you
// authenticate every inbound delivery (hmac-sha256 over the standard webhook headers).
Each delivery is a FactStateDeltaEvent: the source, old_version_sha256 → new_version_sha256, a
diff summary, and the facts whose state changed ({fingerprint, text, old_state → new_state, basis}), plus a pointer to the receipt covering the re-evaluation.
Your callback URL is a public HTTPS endpoint, and a delta is an instruction worth forging — "a fact
your agent relies on just flipped". verifyWebhookSignature is the receiving half of Kaval's
signature: HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<raw body>, compared in constant time.
import express from "express";
import type { FactStateDeltaEvent } from "@usekaval/kaval";
import { verifyWebhookSignature } from "@usekaval/kaval/verify";
// webhook-key-id → that generation's secret. During a rotation overlap BOTH generations sign real
// deliveries, so hold both here and the rollover is a config change instead of an outage.
const secrets = {
[process.env.KAVAL_WEBHOOK_KEY_ID!]: process.env.KAVAL_WEBHOOK_SECRET!,
};
const seen = new Set<string>(); // illustrative; a real receiver dedupes in its database
const app = express();
// express.raw, NOT express.json: the signature covers the exact bytes on the wire. A body that has
// been parsed and re-serialised has a different MAC, and no genuine delivery would ever verify.
app.post("/hooks/kaval", express.raw({ type: "application/json" }), (req, res) => {
const result = verifyWebhookSignature({
body: req.body, // Buffer, untouched
headers: req.headers,
secrets,
toleranceSeconds: 300, // default; the replay window either side of now
});
if (!result.valid) {
// 400, not 401 — a retry will not make an unsigned request signed. `result.reason` is one of
// missing_header · malformed_timestamp · unknown_key_id · unsupported_signature_version ·
// malformed_signature · signature_mismatch · timestamp_out_of_tolerance, and is safe to log.
return res.status(400).json({ error: result.reason });
}
// Delivery is at-least-once by design: a retry after your 500 is a legitimate duplicate. Dedupe
// on result.webhookId (the event's own id) before doing anything with side effects.
if (seen.has(result.webhookId)) return res.status(200).end();
seen.add(result.webhookId);
const event = JSON.parse(req.body.toString("utf8")) as FactStateDeltaEvent;
const { source, old_version_sha256, new_version_sha256, diff_summary, facts } = event.data;
for (const fact of facts) {
// "Aetna requires prior auth for CPT 12345" — holds → changed, at critical materiality.
console.log(
`${fact.materiality} ${fact.old_state} → ${fact.new_state}: ${fact.text}`,
`via ${source.locator} (${old_version_sha256?.slice(0, 12)} → ${new_version_sha256.slice(0, 12)})`,
fact.basis.map((ref) => ref.source_locator),
);
}
void diff_summary; // changed_sections + stats, if you want to show what moved in the document
// Answer 2xx quickly and do the work after; Kaval retries non-2xx with backoff, then dead-letters.
res.status(202).end();
});
The verifier lives on the /verify subpath, so a receiver that never constructs a client pulls in no
HTTP code — and, like the receipt verifier, it never contacts Kaval to decide whether a request is
genuine.
await kaval.listWebhooks();
await kaval.setWebhookEnabled(subscription.subscription_id, false);
await kaval.deleteWebhook(subscription.subscription_id);
// The delivery log is the only place a delivery id is published — start here, then replay.
const { items, next_before } = await kaval.listWebhookDeliveries(
subscription.subscription_id,
{ limit: 100 }, // 1–200, default 50; page with `before`
);
for (const delivery of items) {
if (delivery.state === "dead_letter") await kaval.replayWebhookDelivery(delivery.delivery_id);
}
// Roll the signing key; the old generation keeps verifying until `overlap_until`, so you can
// redeploy the receiver without dropping deliveries. The new secret is shown exactly once.
await kaval.rotateWebhookSigningKey(subscription.subscription_id, {
overlap_until: new Date(Date.now() + 24 * 3_600_000).toISOString(),
});
// createWebhook() is the general form if you need a non-fact_state family.
const { changed, facts_pending_review } = await kaval.sendEvent({
namespace: "contracts",
document_id: "acme-2026-msa",
content: extractedText, // or content_url
scope_keys: ["contract:acme-2026"],
});
Kaval stores the version, diffs it against the previous one, marks the dependent facts stale,
re-evaluates them in the background, and fires the delta webhook. changed: false means the content
was byte-identical: no version row, no staleness, no delta. Checks that land mid-re-evaluation
honestly return REVIEW.
await kaval.reportOutcome({ id: result.receipt.id, kind: "relied_and_correct" });
Kinds: relied_and_correct · current_later_contradicted · stale_caught_real ·
stale_was_false_alarm.
verify() checks one load-bearing conclusion against evidence references you supply and returns a
signed ProofPacket receipt. It is kept only while existing pilot integrations migrate and will be
removed. New code should call check(), which needs no evidence list, is answered from watched
state in milliseconds, and keeps monitoring the facts afterwards.
const { status, receipt } = await kaval.verify({
conclusion: "The 2024 International Building Code is the current IBC edition.",
evidence_refs: ["https://codes.iccsafe.org/content/IBC2024V2.0"],
});
Each item in evidence_refs (1–20 entries) is either a plain https URL string or a strict
{ url, document_id } object; document_id values must be unique per request. A bare { url }
object without document_id is invalid — pass the plain string instead. The client rejects these
wire-invalid shapes locally before spending a request.
| class | when |
|---|---|
KavalError | any non-2xx. Carries status, payload, and idempotencyKey where one was spent. |
KavalRetiredError | HTTP 410 tool_retired — you called a route that folded into /v1/check. Exposes .replacement and a message naming check(). |
TypeError | locally-detected invalid input, thrown before any network call |
verify() is the only method that spends an operation key: it sends a fresh UUID
Idempotency-Key and retries once with the same key when the connection fails without a
trustworthy response, or the API says the operation is still being finalized. It does not retry
ordinary API errors, rate limits, or terminal 5xx responses. If both bounded attempts stay
ambiguous, the thrown error exposes error.idempotencyKey — pass it back explicitly after your own
delay to resume the same operation instead of billing a new one.
createWebhook() (and subscribeFactStateDeltas()) send a key because the API requires one; they
generate it when you do not supply it.
check() deliberately sends none: it is a read of current state, so a retry recomputes rather than
replays and cannot double-bill.
Every method — health() included — accepts a final { idempotencyKey?, signal?, timeoutMs? }. The
constructor defaults to a 150-second deadline; override per call or set timeoutMs: null to disable
it. That default matches the API's own handler deadline: a client-side deadline below the server's
research budget does not bound anything, it just aborts your own cold check(). To finish sooner,
send a smaller max_wait_ms (or mode: "fast") and get a real verdict instead of an AbortError.
| 0.5 | 0.6 |
|---|---|
audit() | check() — the receipt is the proof |
gate() / gateAction() | check() — the warm path re-checks in ~50ms; there is nothing to re-apply |
check(belief) | check({ action }) |
verifyBelief() | check({ action, context }) — branch on decision === "ALLOW", not act |
extractAndCheck({ text }) | check({ action: text }) — Kaval compiles the facts itself |
scanStore({ beliefs }) | check({ claims }) — up to 20 per call |
monitor({ beliefs, webhook }) | addSource() + subscribeFactStateDeltas() — deltas are pushed, not swept |
kaval() / kavalBatch() | check({ claims: [{ subject, predicate, object, scope }] }) |
verify() | verify(), deprecated → check() |
ProofNotFoundError | removed with /v1/gate |
| — | new: getReceipt, source registry, sendEvent, webhook subscriptions |
Status mapping: current + act: true → decision: "ALLOW" with every fact holds;
stale/contradicted → a fact changed (REVIEW or BLOCK by materiality);
unsupported/insufficient/conflicting → a fact unknown (REVIEW, or BLOCK if critical).
check · getReceipt · addSource · listSources · getSource · pauseSource · resumeSource ·
recompileSource · deleteSource · sendEvent · subscribeFactStateDeltas · createWebhook ·
listWebhooks · setWebhookEnabled · deleteWebhook · listWebhookDeliveries ·
rotateWebhookSigningKey · replayWebhookDelivery · reportOutcome · verify (deprecated) ·
health.
Construct with { apiKey, baseUrl?, fetch?, timeoutMs? } — baseUrl defaults to
https://api.usekaval.com. Works in Node 18+, browsers, and edge runtimes (uses the global fetch).
Offline verification is a separate surface with no client and no key:
@usekaval/kaval/verify — verifyReceipt for receipts,
verifyWebhookSignature for inbound deliveries — plus the kaval-receipt-verify command.
Env vars: this package does not read KAVAL_BASE_URL from the environment — pass
baseUrl in the constructor (Python SDK and MCP use KAVAL_BASE_URL; the marketing-site proxy
uses KAVAL_API_URL). See the clients README.
Demo results carry no organizational authority. A production ALLOW requires a customer-bound
action policy and applicable empirical calibration; REVIEW is never permission to act.
The Python client mirrors this surface: pip install kaval.
FAQs
Before an AI agent acts, Kaval verifies the facts the action depends on and returns ALLOW, REVIEW, or BLOCK with an Ed25519-signed receipt.
The npm package @usekaval/kaval receives a total of 240 weekly downloads. As such, @usekaval/kaval popularity was classified as not popular.
We found that @usekaval/kaval demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.

Research
/Security News
Popular npm packages keyv and cacheable compromised.

Security News
A misconfiguration gave three Anthropic models internet access, and one, believing it was in a simulation, shipped a credential-stealing package to PyPI.