@coproduct_inc/verify 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  All notable changes to `@coproduct_inc/verify`.
4
4
 
5
+ ## 0.6.0
6
+
7
+ - **The `Workflow` flow check now CONSUMES the one proven IFC model** —
8
+ `nucleus-ifc`'s `FlowDeclaration → IfcVerdict` decision, compiled into the
9
+ package's wasm and exposed as `ifcDecide` (`./ifc-decide`). The DAG walk
10
+ accumulates each node's declared inputs (`secret`, `web_content`, …) and asks
11
+ the proven `decide()` whether they may reach a sink — the same decision
12
+ nucleus's production gate runs, recomputed locally. No TS mirror in the
13
+ decision path.
14
+ - New `FlowPolicy` axes `adversarialSources` + `actionSinks` enable **indirect
15
+ prompt-injection** detection: web content (Adversarial integrity, NoAuthority)
16
+ reaching an action sink is refused across the DAG; a declassifier breaks the
17
+ path. Existing confidentiality policies are unchanged.
18
+ - **IFC lattice helpers** (`./ifc`) — a TS reflection of the same Denning lattice
19
+ for direct in-process use, kept **parity-checked against `ifcDecide`** (and the
20
+ Lean-proven laws via `ifcLatticeLaws()`).
21
+ - Build: `nucleus-wasm` now depends on `nucleus-ifc` (lattice-only, no `ring`) and
22
+ exports `ifc_decide`; the `build:wasm` script preserves the wasm dir's
23
+ `commonjs` marker so the CJS bindings load inside this ESM package.
24
+
25
+ ## 0.5.0
26
+
27
+ - **`Workflow` — the DAG cage** (`./workflow`, re-exported from root). Compose a
28
+ multi-agent run into ONE signed `WorkflowReceipt`: register nodes (each a
29
+ `Cage`) with `after` edges, then `wf.attest(privateKey)`. `verifyWorkflowReceipt`
30
+ checks it offline — signature + per-node in-bounds recompute + the DAG (acyclic,
31
+ real edges) + a **cross-node information-flow policy** (`{ sources, sinks,
32
+ declassifiers }`). The keystone: every node can be locally in-bounds yet the
33
+ workflow refuses a run where a secret laundered source→…→sink across the DAG —
34
+ the failure that only exists at multi-agent scale, caught at the orchestration
35
+ layer. See `examples/workflow-leak.mjs`.
36
+
37
+ ## 0.4.0
38
+
39
+ - **`Cage` — the 5-minute producer drop-in** (`./cage`, re-exported from root).
40
+ A runtime cage wraps an agent's tool calls (`cage.tool(name, fn, { guard,
41
+ subject })`): out-of-boundary calls are blocked, every call is recorded, and
42
+ `cage.attest(privateKey)` emits a signed in-bounds receipt the existing
43
+ `verifyReceipt` / CLI checks offline. Closes the loop — producers cage,
44
+ consumers verify, nobody trusts the seller. See `examples/cage-quickstart.mjs`.
45
+
5
46
  ## 0.3.0
6
47
 
7
48
  ### Added
package/README.md CHANGED
@@ -12,6 +12,77 @@ Offline, **zero-trust** verification for Nucleus. Two receipt families share one
12
12
 
13
13
  ---
14
14
 
15
+ ## Cage your agent — the 5-minute drop-in
16
+
17
+ You ship an agent that takes actions for a customer. They won't take your word
18
+ that it stayed in bounds. **Cage its tool calls; hand them a receipt they verify
19
+ themselves.** Useful to you alone today (a tighter agent + an artifact your
20
+ customer can check); more valuable as a network of recomputable receipts forms.
21
+
22
+ ```ts
23
+ import { Cage, generateKeypair, verifyReceipt } from "@coproduct_inc/verify";
24
+
25
+ const cage = new Cage({
26
+ principal: "spiffe://acme/support-agent", // a stable id, not a name
27
+ allowedTools: ["search", "read_ticket", "refund"], // its capability boundary
28
+ });
29
+
30
+ // Wrap each tool. Out-of-boundary calls are blocked; a guard adds a runtime
31
+ // check (never refund over $50). Works for sync and async tools.
32
+ const search = cage.tool("search", realSearch);
33
+ const refund = cage.tool("refund", realRefund, { guard: (cents) => cents <= 5000 });
34
+ const wire = cage.tool("wire_transfer", realWire); // NOT granted
35
+
36
+ await search("refund policy"); // recorded: allow
37
+ try { await wire("0xbad"); } catch {} // BLOCKED + recorded — your customer sees the attempt
38
+
39
+ // Emit a signed receipt and hand it over:
40
+ const { privateKey } = generateKeypair();
41
+ const receipt = cage.attest(privateKey);
42
+
43
+ // THEY verify it — offline, no trust in you (or: npx @coproduct_inc/verify receipt.json):
44
+ const report = verifyReceipt(receipt); // report.ok === clean run, in bounds, signature valid
45
+ ```
46
+
47
+ Run it end to end: `node examples/cage-quickstart.mjs`. The receipt attests the
48
+ **tool boundary** (every call was to a permitted tool); for argument-level
49
+ *proofs* ("amount < cap over all inputs") see the bytecode prover.
50
+
51
+ ### Multi-agent? Compose the DAG — and catch what node-checks can't
52
+
53
+ A single agent you can eyeball; a DAG of agents you can't. The dangerous failure
54
+ only exists at DAG scale: **every node is locally in-bounds, yet a secret
55
+ launders from a source, through an innocent middle, to a public sink.** Per-node
56
+ review all passes; only the whole-workflow check catches it.
57
+
58
+ ```ts
59
+ import { Workflow, verifyWorkflowReceipt, generateKeypair } from "@coproduct_inc/verify";
60
+
61
+ const wf = new Workflow("spiffe://acme/intake-pipeline");
62
+ const reader = wf.node({ id: "reader", boundary: ["read_secret", "emit"] });
63
+ const enrich = wf.node({ id: "enricher", boundary: ["transform", "emit"], after: ["reader"] });
64
+ const pub = wf.node({ id: "publisher", boundary: ["publish"], after: ["enricher"] });
65
+ // …run your agents through reader/enrich/pub's caged tools…
66
+
67
+ const receipt = wf.attest(generateKeypair().privateKey);
68
+
69
+ verifyWorkflowReceipt(receipt, { sources: ["read_secret"], sinks: ["publish"] });
70
+ // → nodesOk: true (every node in-bounds) … but ok: false, flowOk: false:
71
+ // "information-flow leak: publisher→publish" — the secret reached the sink across the DAG.
72
+ ```
73
+
74
+ It rides on whatever orchestrator you already use (LangGraph, Temporal, CrewAI…)
75
+ — you cage the tools, not the framework. Run it: `node examples/workflow-leak.mjs`.
76
+
77
+ The flow check is grounded in the **Denning IFC lattice** (`./ifc`, a faithful
78
+ port of `nucleus-ifc` whose lattice laws are Lean-proven). Beyond confidentiality
79
+ (`sources`/`sinks`), the `adversarialSources` + `actionSinks` axes catch
80
+ **indirect prompt injection** — web content (Adversarial integrity, no authority
81
+ to instruct) reaching an action sink is refused across the DAG, and a
82
+ declassifier breaks the path.
83
+
84
+ ---
85
+
15
86
  ## In-bounds attestation — the drop-in
16
87
 
17
88
  ```ts
@@ -0,0 +1 @@
1
+ export { verifyReceipt, verifyReceiptJson, recomputeVerdict, canonicalize, rootHash, makeBoundary, permissionFingerprint, } from "./attestation.js";
@@ -0,0 +1,3 @@
1
+ // Browser-only entry: the SAME in-bounds attestation verifier, with node:crypto
2
+ // aliased to a @noble shim at bundle time. Verify-only (no signReceipt).
3
+ export { verifyReceipt, verifyReceiptJson, recomputeVerdict, canonicalize, rootHash, makeBoundary, permissionFingerprint, } from "./attestation.js";
package/dist/cage.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ import type { Receipt } from "./attestation.js";
3
+ import { type ToolProxyExport } from "./toolproxy.js";
4
+ /** Thrown when a caged tool call violates policy (and is blocked). */
5
+ export declare class CageDenied extends Error {
6
+ readonly tool: string;
7
+ constructor(tool: string, message: string);
8
+ }
9
+ export interface CageOptions {
10
+ /** Cryptographic principal the receipt is keyed on — a stable id (SPIFFE id,
11
+ * key fingerprint, anything), NOT a mutable display name. */
12
+ principal: string;
13
+ /** The tools this agent is permitted to call — its capability boundary. A call
14
+ * to anything else is denied and recorded. */
15
+ allowedTools: string[];
16
+ }
17
+ export interface ToolOptions<A extends unknown[]> {
18
+ /** A per-call predicate to deny an in-boundary tool on specific arguments
19
+ * (e.g. only allow `pay` below a cap). Returning false → blocked at runtime +
20
+ * recorded as a deny. NOTE: the receipt attests the TOOL boundary (tool ∈
21
+ * allowedTools); a guard is finer RUNTIME hardening — a guard-denied call to
22
+ * an allowed tool is still "in bounds" in the receipt (the tool was permitted).
23
+ * Argument-level *proof* (e.g. "amount < cap over all inputs") is the
24
+ * bytecode prover's job, not this receipt's. */
25
+ guard?: (...args: A) => boolean;
26
+ /** Bind the call to its target (a URL, path, counterparty…) — recorded as a
27
+ * digest in the receipt, so the receipt is specific to what was acted on. */
28
+ subject?: (...args: A) => string;
29
+ }
30
+ /**
31
+ * A runtime cage for an agent's tool calls. Wrap each tool with {@link Cage.tool};
32
+ * every call is policy-checked and recorded, and a denied call is BLOCKED. At the
33
+ * end {@link Cage.attest} signs an in-bounds receipt verifiable offline by this
34
+ * same package.
35
+ */
36
+ export declare class Cage {
37
+ private readonly opts;
38
+ private readonly records;
39
+ constructor(opts: CageOptions);
40
+ /** Wrap a tool function so each call is caged + recorded. Works for sync and
41
+ * async tools (a denied call throws `CageDenied` before the tool runs). */
42
+ tool<A extends unknown[], R>(name: string, fn: (...args: A) => R, options?: ToolOptions<A>): (...args: A) => R;
43
+ /** Record a call you invoke yourself (when wrapping the fn isn't convenient). */
44
+ record(name: string, allowed: boolean, subject?: string): void;
45
+ /** How many calls have been caged so far. */
46
+ get callCount(): number;
47
+ /** The raw verdict log — the tool-proxy export the verifier consumes. */
48
+ toExport(): ToolProxyExport;
49
+ /** Sign an in-bounds attestation receipt. Verify it anywhere with
50
+ * `verifyReceipt` / the `@coproduct_inc/verify` CLI — no trust in you required. */
51
+ attest(privateKey: KeyObject, opts?: {
52
+ signedAt?: string;
53
+ }): Receipt;
54
+ }
package/dist/cage.js ADDED
@@ -0,0 +1,66 @@
1
+ import { attestToolProxyRun } from "./toolproxy.js";
2
+ /** Thrown when a caged tool call violates policy (and is blocked). */
3
+ export class CageDenied extends Error {
4
+ tool;
5
+ constructor(tool, message) {
6
+ super(message);
7
+ this.tool = tool;
8
+ this.name = "CageDenied";
9
+ }
10
+ }
11
+ /**
12
+ * A runtime cage for an agent's tool calls. Wrap each tool with {@link Cage.tool};
13
+ * every call is policy-checked and recorded, and a denied call is BLOCKED. At the
14
+ * end {@link Cage.attest} signs an in-bounds receipt verifiable offline by this
15
+ * same package.
16
+ */
17
+ export class Cage {
18
+ opts;
19
+ records = [];
20
+ constructor(opts) {
21
+ this.opts = opts;
22
+ }
23
+ /** Wrap a tool function so each call is caged + recorded. Works for sync and
24
+ * async tools (a denied call throws `CageDenied` before the tool runs). */
25
+ tool(name, fn, options) {
26
+ return (...args) => {
27
+ const inBoundary = this.opts.allowedTools.includes(name);
28
+ const guardOk = !options?.guard || options.guard(...args);
29
+ const ok = inBoundary && guardOk;
30
+ const rec = { operation: name, verdict: ok ? "allow" : "deny", actor: this.opts.principal };
31
+ const subject = options?.subject?.(...args);
32
+ if (subject)
33
+ rec.subject = subject;
34
+ this.records.push(rec);
35
+ if (!ok) {
36
+ throw new CageDenied(name, inBoundary
37
+ ? `tool "${name}" denied by its guard on these arguments`
38
+ : `tool "${name}" is outside the agent's capability boundary {${this.opts.allowedTools.join(", ")}}`);
39
+ }
40
+ return fn(...args);
41
+ };
42
+ }
43
+ /** Record a call you invoke yourself (when wrapping the fn isn't convenient). */
44
+ record(name, allowed, subject) {
45
+ const rec = { operation: name, verdict: allowed ? "allow" : "deny", actor: this.opts.principal };
46
+ if (subject)
47
+ rec.subject = subject;
48
+ this.records.push(rec);
49
+ }
50
+ /** How many calls have been caged so far. */
51
+ get callCount() {
52
+ return this.records.length;
53
+ }
54
+ /** The raw verdict log — the tool-proxy export the verifier consumes. */
55
+ toExport() {
56
+ return {
57
+ boundary: { principal: this.opts.principal, allowedTools: this.opts.allowedTools },
58
+ records: [...this.records],
59
+ };
60
+ }
61
+ /** Sign an in-bounds attestation receipt. Verify it anywhere with
62
+ * `verifyReceipt` / the `@coproduct_inc/verify` CLI — no trust in you required. */
63
+ attest(privateKey, opts) {
64
+ return attestToolProxyRun(this.toExport(), privateKey, opts).receipt;
65
+ }
66
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nucleus-verify-claim` — the **proof gate**. Adjudicate one or more
4
+ * recompute-verified claims and use the exit code as a CI/merge gate.
5
+ *
6
+ * 0 → every claim verified (or no claim files found, unless --require)
7
+ * 1 → at least one claim failed verification
8
+ * 2 → usage / I/O error
9
+ *
10
+ * Usage:
11
+ * nucleus-verify-claim <claim.json...> [--summary <file>] [--json]
12
+ * [--require] [--quiet]
13
+ *
14
+ * Each `<claim.json>` holds a `Claim` (or an array of them). Evidence may be
15
+ * inlined (`evidence.receipt`) or referenced by path (`evidence.receiptPath`,
16
+ * resolved relative to the claim file) so a repo commits a small claim that
17
+ * points at a checked-in receipt.
18
+ *
19
+ * Designed as a drop-in GitHub Action step: writes a Markdown table to
20
+ * `--summary` (point it at `$GITHUB_STEP_SUMMARY`) and a one-line-per-claim
21
+ * log, then fails the job if any claim did not recompute.
22
+ */
23
+ export {};
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nucleus-verify-claim` — the **proof gate**. Adjudicate one or more
4
+ * recompute-verified claims and use the exit code as a CI/merge gate.
5
+ *
6
+ * 0 → every claim verified (or no claim files found, unless --require)
7
+ * 1 → at least one claim failed verification
8
+ * 2 → usage / I/O error
9
+ *
10
+ * Usage:
11
+ * nucleus-verify-claim <claim.json...> [--summary <file>] [--json]
12
+ * [--require] [--quiet]
13
+ *
14
+ * Each `<claim.json>` holds a `Claim` (or an array of them). Evidence may be
15
+ * inlined (`evidence.receipt`) or referenced by path (`evidence.receiptPath`,
16
+ * resolved relative to the claim file) so a repo commits a small claim that
17
+ * points at a checked-in receipt.
18
+ *
19
+ * Designed as a drop-in GitHub Action step: writes a Markdown table to
20
+ * `--summary` (point it at `$GITHUB_STEP_SUMMARY`) and a one-line-per-claim
21
+ * log, then fails the job if any claim did not recompute.
22
+ */
23
+ import { readFileSync, writeFileSync, appendFileSync } from "node:fs";
24
+ import { dirname, resolve } from "node:path";
25
+ import { verifyClaim } from "./claim.js";
26
+ const HELP = `nucleus-verify-claim — recompute-verified proof gate
27
+
28
+ USAGE:
29
+ nucleus-verify-claim <claim.json...> [options]
30
+
31
+ OPTIONS:
32
+ --summary <file> append a Markdown result table to <file>
33
+ (use "$GITHUB_STEP_SUMMARY" inside a GitHub Action)
34
+ --json print the full verdicts as JSON to stdout
35
+ --require fail (exit 1) if NO claim files were supplied/matched
36
+ --quiet suppress per-claim log lines (exit code + summary only)
37
+ -h, --help show this help
38
+
39
+ EXIT CODES:
40
+ 0 all claims verified 1 a claim failed / none found with --require 2 usage / I/O error`;
41
+ function parseArgs(argv) {
42
+ const args = { paths: [], json: false, require: false, quiet: false, help: false };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ switch (a) {
46
+ case "-h":
47
+ case "--help":
48
+ args.help = true;
49
+ break;
50
+ case "--json":
51
+ args.json = true;
52
+ break;
53
+ case "--require":
54
+ args.require = true;
55
+ break;
56
+ case "--quiet":
57
+ args.quiet = true;
58
+ break;
59
+ case "--summary":
60
+ args.summary = argv[++i];
61
+ break;
62
+ default:
63
+ if (a.startsWith("--"))
64
+ throw new Error(`unknown option ${a}`);
65
+ args.paths.push(a);
66
+ }
67
+ }
68
+ return args;
69
+ }
70
+ /** Load the claims from one file, resolving any `receiptPath` against it. */
71
+ function loadClaims(path) {
72
+ const raw = JSON.parse(readFileSync(path, "utf8"));
73
+ const entries = Array.isArray(raw) ? raw : [raw];
74
+ return entries.map((entry) => {
75
+ const ev = entry.evidence ?? {};
76
+ if (ev.receiptPath && ev.receipt === undefined) {
77
+ const receiptText = readFileSync(resolve(dirname(path), ev.receiptPath), "utf8");
78
+ // Keep object receipts as objects (in_bounds) and string receipts as
79
+ // strings (clearing wire form) — try to parse, fall back to raw text.
80
+ let receipt;
81
+ try {
82
+ receipt = JSON.parse(receiptText);
83
+ }
84
+ catch {
85
+ receipt = receiptText;
86
+ }
87
+ return { ...entry, evidence: { ...ev, receipt } };
88
+ }
89
+ return entry;
90
+ });
91
+ }
92
+ function statusCell(v) {
93
+ return v.verified ? "✓ verified" : `✗ ${v.reason ?? "failed"}`;
94
+ }
95
+ function writeSummary(file, verdicts) {
96
+ const failed = verdicts.filter((v) => !v.verified).length;
97
+ const lines = [
98
+ "## Proof gate — recompute-verified claims",
99
+ "",
100
+ "| claim | kind | verdict |",
101
+ "| --- | --- | --- |",
102
+ ...verdicts.map((v) => `| ${escapePipes(v.statement)} | \`${v.kind}\` | ${escapePipes(statusCell(v))} |`),
103
+ "",
104
+ failed === 0
105
+ ? `**✓ all ${verdicts.length} claim${verdicts.length === 1 ? "" : "s"} recompute-verified.**`
106
+ : `**✗ ${failed} of ${verdicts.length} claim${verdicts.length === 1 ? "" : "s"} failed.**`,
107
+ "",
108
+ ];
109
+ // `$GITHUB_STEP_SUMMARY` is appended to across steps; honor that.
110
+ const body = lines.join("\n");
111
+ try {
112
+ appendFileSync(file, body + "\n");
113
+ }
114
+ catch {
115
+ writeFileSync(file, body + "\n");
116
+ }
117
+ }
118
+ function escapePipes(s) {
119
+ return s.replace(/\|/g, "\\|");
120
+ }
121
+ async function main() {
122
+ let args;
123
+ try {
124
+ args = parseArgs(process.argv.slice(2));
125
+ }
126
+ catch (e) {
127
+ process.stderr.write(`error: ${e.message}\n\n${HELP}\n`);
128
+ return 2;
129
+ }
130
+ if (args.help) {
131
+ process.stdout.write(HELP + "\n");
132
+ return 0;
133
+ }
134
+ if (args.paths.length === 0) {
135
+ if (args.require) {
136
+ process.stderr.write("error: no claim files supplied (--require)\n");
137
+ return 1;
138
+ }
139
+ process.stdout.write("proof gate: no claims to verify.\n");
140
+ return 0;
141
+ }
142
+ let claims;
143
+ try {
144
+ claims = args.paths.flatMap(loadClaims);
145
+ }
146
+ catch (e) {
147
+ process.stderr.write(`error: cannot read claims: ${e.message}\n`);
148
+ return 2;
149
+ }
150
+ const verdicts = [];
151
+ for (const claim of claims) {
152
+ verdicts.push(await verifyClaim(claim));
153
+ }
154
+ if (args.json) {
155
+ process.stdout.write(JSON.stringify(verdicts, null, 2) + "\n");
156
+ }
157
+ else if (!args.quiet) {
158
+ for (const v of verdicts) {
159
+ const mark = v.verified ? "✓" : "✗";
160
+ const stream = v.verified ? process.stdout : process.stderr;
161
+ stream.write(`${mark} [${v.kind}] ${v.statement} — ${statusCell(v)}\n`);
162
+ if (!v.verified) {
163
+ for (const step of v.trace.filter((s) => !s.ok)) {
164
+ stream.write(` ↳ ${step.check}${step.detail ? `: ${step.detail}` : ""}\n`);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ if (args.summary) {
170
+ try {
171
+ writeSummary(args.summary, verdicts);
172
+ }
173
+ catch (e) {
174
+ process.stderr.write(`warning: could not write summary: ${e.message}\n`);
175
+ }
176
+ }
177
+ const failed = verdicts.filter((v) => !v.verified).length;
178
+ if (!args.quiet) {
179
+ process.stdout.write(failed === 0
180
+ ? `proof gate: ✓ ${verdicts.length} verified.\n`
181
+ : `proof gate: ✗ ${failed} of ${verdicts.length} failed.\n`);
182
+ }
183
+ return failed === 0 ? 0 : 1;
184
+ }
185
+ main().then((code) => process.exit(code));
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `claim.ts` — a **recompute-verified claim** façade.
3
+ *
4
+ * One uniform call — `verifyClaim(claim) → ClaimVerdict` — over the package's
5
+ * existing recompute engines, so any surface (a comment-box widget, a CI
6
+ * check, an MCP tool) can render a single ✓/✗ plus a human-readable recompute
7
+ * trace next to a claim, without knowing which engine adjudicated it.
8
+ *
9
+ * ## The honest boundary
10
+ *
11
+ * This verifies **only** claims that reduce to a *deterministic recomputation
12
+ * over committed evidence* — never arbitrary prose. A claim is adjudicable iff
13
+ * its value is the output of a pure function the caller can re-run from the
14
+ * cited inputs:
15
+ *
16
+ * - `clearing` — re-run an auction's clearing from its SIGNED bid set and
17
+ * check the receipt's price/winner (WASM kernel; the exact
18
+ * `verify` / `recomputeClearing` path).
19
+ * - `in_bounds` — re-run an agent run's capability verdict from its tool
20
+ * trace + declared boundary (pure-node attestation engine).
21
+ * - `signature` — authenticity only: Ed25519 + BLAKE3 root hash, no
22
+ * value recompute.
23
+ *
24
+ * Anything NOT reducible to a recompute (e.g. "10k users downloaded this")
25
+ * returns `verified: false` with an explicit `unsupported`/`reason` — it is
26
+ * never silently passed. `eval_score` and `dataset_aggregate` are the planned
27
+ * next kinds (see docs/RECOMPUTE-VERIFIED-CLAIMS.md); until wired they report
28
+ * unsupported rather than faking a check.
29
+ *
30
+ * ## Proof-carrying numbers
31
+ *
32
+ * When a claim carries an `asserted` value (the number a human wrote in prose,
33
+ * e.g. "cleared at 400000 µ$"), it is cross-checked against the recomputed
34
+ * value. A receipt that is authentic but whose prose number was altered fails
35
+ * (`verified: false`, `asserted.matched: false`) — the receipt can't lie, and
36
+ * neither can the sentence quoting it.
37
+ */
38
+ /** The closed set of recomputable claim kinds. */
39
+ export type ClaimKind = "clearing" | "in_bounds" | "signature";
40
+ /** Evidence a claim is recomputed against. Shape depends on `kind`. */
41
+ export interface ClaimEvidence {
42
+ /**
43
+ * For `clearing` / `signature`: the auction receipt as a JSON **string**
44
+ * (the hub's wire form). For `in_bounds`: the attestation receipt **object**
45
+ * (as produced by `signReceipt` / returned from a tool-proxy run).
46
+ */
47
+ receipt: string | Record<string, unknown>;
48
+ /** Pinned issuer JWKS JSON string. Required for `signature`; for `clearing`
49
+ * it upgrades the check from recompute-only to recompute + signature. */
50
+ jwks?: string;
51
+ /** `in_bounds`: pin the expected cryptographic principal / public key. */
52
+ expectPrincipal?: string;
53
+ expectPublicKey?: string;
54
+ }
55
+ /** An author-asserted value to cross-check against the recompute. */
56
+ export interface ClaimAssertion {
57
+ /** `clearing`: integer micro-USD price. `in_bounds`: boolean. */
58
+ value: number | boolean | string;
59
+ }
60
+ export interface Claim {
61
+ /** Human-readable statement rendered next to the ✓/✗. */
62
+ statement: string;
63
+ kind: ClaimKind;
64
+ evidence: ClaimEvidence;
65
+ /** Optional prose value to cross-check (proof-carrying numbers). */
66
+ asserted?: ClaimAssertion;
67
+ }
68
+ export interface ClaimTraceStep {
69
+ check: string;
70
+ ok: boolean;
71
+ detail?: string;
72
+ }
73
+ export interface ClaimVerdict {
74
+ /** The single boolean to gate the ✓/✗ on. */
75
+ verified: boolean;
76
+ kind: ClaimKind;
77
+ statement: string;
78
+ /** What the recompute produced (kind-specific), or null on a hard error. */
79
+ recomputed: {
80
+ label: string;
81
+ value: unknown;
82
+ } | null;
83
+ /** The author's asserted value + whether it matched the recompute. */
84
+ asserted: {
85
+ value: unknown;
86
+ matched: boolean;
87
+ } | null;
88
+ /** Ordered, human-readable trace of the checks that ran. */
89
+ trace: ClaimTraceStep[];
90
+ /** First failing reason, or null when verified. */
91
+ reason: string | null;
92
+ }
93
+ /**
94
+ * Adjudicate a claim by recomputation. Never throws — a malformed receipt,
95
+ * an unknown kind, or an engine error surface as `verified: false` with a
96
+ * `reason`, so the widget can always render a definite ✓/✗.
97
+ */
98
+ export declare function verifyClaim(claim: Claim): Promise<ClaimVerdict>;