tersign 0.4.3 → 0.4.5
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/README.md +3 -1
- package/dist/canonical.d.ts +55 -0
- package/dist/canonical.js +74 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +94 -39
- package/dist/verify-bin.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ The text is digested **locally** (only the digest travels — data-minimization
|
|
|
47
47
|
|
|
48
48
|
Every entry takes the same path: the seller **signs** the receipt (EIP-712, x402 offer-receipt extension) → Tersign computes the **keccak256 canonical digest** → the digest joins that **seller's hash chain**, each `seq n` bound to `seq n−1` → the neutral ledger **counter-signs** (secp256k1) → **anyone verifies**, and any venue gets a serialized envelope.
|
|
49
49
|
|
|
50
|
+
Since 2026-08-28 each anchor stamps a chain commitment — an accumulator over every counter-signed link — so one anchored digest covers the whole prefix; rows anchored earlier bind the head record only and say so (`subjectSchema`).
|
|
51
|
+
|
|
50
52
|
```mermaid
|
|
51
53
|
graph LR
|
|
52
54
|
A["agent transaction<br/>x402"] --> B["seller-signed receipt<br/>EIP-712"]
|
|
@@ -137,7 +139,7 @@ The agent skill `tersign-evidence` ships at [tersignhq/skills](https://github.co
|
|
|
137
139
|
- **Ledger + dashboard** — public verify page: https://tersign.ai/verify
|
|
138
140
|
- **Census** — hash-chained observations across the live x402 seller catalog, probed autonomously; the numbers are served live, never quoted stale: https://prober.tersign.ai/v1/prober/stats
|
|
139
141
|
- **Conformance** — RFC 8785 (JCS) canonical serialization, keccak256 digests, and the public two-sided vector suite (canonical bytes, number domain, content address, chain continuity, completeness, anchored existence, phase separation, offer binding, independence — every criterion carrying both an accepting and an adversarial vector): [tersignhq/evidence-record-conformance](https://github.com/tersignhq/evidence-record-conformance). Reproduce the bytes and your implementation is conformant — in any language.
|
|
140
|
-
- **Standards** — the `compliance-fields` extension — a typed compliance-record schema plus four evaluator-side disqualifications (independence, completeness/existence, economic-phase separation, and commitment scope — an independence claim reaches exactly as far as the record's own commitments), each executable as a two-sided conformance vector — is under review upstream ([x402-foundation/x402#2853](https://github.com/x402-foundation/x402/pull/2853)) and referenced in the x402 TSC's evidence-record charter agenda ([tsc#4](https://github.com/x402-foundation/tsc/issues/4)). The merged offer-receipt spec already carries
|
|
142
|
+
- **Standards** — the `compliance-fields` extension — a typed compliance-record schema plus four evaluator-side disqualifications (independence, completeness/existence, economic-phase separation, and commitment scope — an independence claim reaches exactly as far as the record's own commitments), each executable as a two-sided conformance vector — is under review upstream ([x402-foundation/x402#2853](https://github.com/x402-foundation/x402/pull/2853)) and referenced in the x402 TSC's evidence-record charter agenda ([tsc#4](https://github.com/x402-foundation/tsc/issues/4)). The merged offer-receipt spec already carries post-session verification guidance — signer authorization evaluated as of `issuedAt`, with mutable-source rotation handled explicitly ([#2811](https://github.com/x402-foundation/x402/pull/2811), merged); the completeness, independence, existence and phase disqualifications are the open extension's normative core.
|
|
141
143
|
|
|
142
144
|
## Machine Surfaces
|
|
143
145
|
|
package/dist/canonical.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
1
2
|
/** Deterministic JSON — RFC 8785 (JCS) conformant for JSON-domain inputs: keys sorted by
|
|
2
3
|
* UTF-16 code units, no whitespace, JSON.stringify scalar/escape semantics. Arrays keep
|
|
3
4
|
* order; undefined properties are dropped (matches JSON.stringify semantics).
|
|
@@ -9,3 +10,57 @@
|
|
|
9
10
|
* digest and all pinned wire vectors are unchanged. */
|
|
10
11
|
export declare function canonicalStringify(value: unknown): string;
|
|
11
12
|
export declare function digestOf(value: unknown): `0x${string}`;
|
|
13
|
+
/** Hash-chain link, byte-identical to the hosted ledger's recompute: the ledger
|
|
14
|
+
* counter-signs `keccak256(artifactDigest || prevDigest-or-32-zero-bytes || uint64be(seq))`.
|
|
15
|
+
* `prevDigest` is the PREVIOUS record's artifactDigest (null at seq 1). */
|
|
16
|
+
export declare const GENESIS_DIGEST: Hex;
|
|
17
|
+
export declare function chainLinkDigest(artifactDigest: Hex, prevDigest: Hex | null, seq: number): Hex;
|
|
18
|
+
/** Chain commitment accumulator (anchored since 2026-08-28). DERIVED, never signed, never
|
|
19
|
+
* stored per row: acc_0 = keccak256(utf8("tersign-chain-commitment-v1")); acc_k =
|
|
20
|
+
* keccak256(acc_{k-1} || link_k). A pure function of (a_1..a_k, 1..k) — any omission,
|
|
21
|
+
* insertion, reordering or rewrite below k changes acc_k, so ONE anchored digest over acc_N
|
|
22
|
+
* commits to the whole prefix. The anchor stamps digestOf({acc, head, schema, seq}).
|
|
23
|
+
* Pinned cross-implementation (this SDK, the hosted ledger, the Python SDK and the evidence-bundle
|
|
24
|
+
* verifier share the same test vectors): edit all or none. */
|
|
25
|
+
export declare const CHAIN_COMMITMENT_SCHEMA = "tersign-chain-commitment-v1";
|
|
26
|
+
export declare const ACC_GENESIS: Hex;
|
|
27
|
+
export declare function chainAccumulatorStep(acc: Hex, link: Hex): Hex;
|
|
28
|
+
export type ChainCommitment = {
|
|
29
|
+
acc: Hex;
|
|
30
|
+
head: Hex;
|
|
31
|
+
schema: typeof CHAIN_COMMITMENT_SCHEMA;
|
|
32
|
+
seq: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function chainCommitment(seq: number, head: Hex, acc: Hex): ChainCommitment;
|
|
35
|
+
/** keccak256 over the JCS bytes of the commitment object — the anchored subject digest. */
|
|
36
|
+
export declare function commitmentDigest(seq: number, head: Hex, acc: Hex): Hex;
|
|
37
|
+
/** A set of records that does not re-walk (gap, prev ≠ previous artifact, wrong seq): no
|
|
38
|
+
* accumulator is derived over it — a value no independent recompute would reproduce. */
|
|
39
|
+
export declare class ChainIntegrityError extends Error {
|
|
40
|
+
}
|
|
41
|
+
export type ChainRecordLike = {
|
|
42
|
+
artifactDigest: Hex;
|
|
43
|
+
prevDigest: Hex | null;
|
|
44
|
+
seq: number;
|
|
45
|
+
};
|
|
46
|
+
/** Fold records 1..N (in seq order, dense, prev-continuous). Returns acc_N and the head
|
|
47
|
+
* artifact digest a_N. Throws ChainIntegrityError on any discontinuity or on an empty set. */
|
|
48
|
+
export declare function foldAccumulator(records: ChainRecordLike[]): {
|
|
49
|
+
acc: Hex;
|
|
50
|
+
head: Hex;
|
|
51
|
+
};
|
|
52
|
+
export type CommitmentVerifyResult = {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
acc: Hex;
|
|
55
|
+
digest: Hex;
|
|
56
|
+
reason?: string;
|
|
57
|
+
};
|
|
58
|
+
/** Recompute the accumulator over `records` (seq 1..N as served by the ledger) and compare it
|
|
59
|
+
* to an anchored commitment (the `commitment` object from `/v1/receipts/{digest}/verify`, or
|
|
60
|
+
* any `{seq, acc, head?}`). `acc`/`digest` are the RECOMPUTED values — compare `digest` to the
|
|
61
|
+
* anchor's subjectDigest. A truncated, substituted, reordered or renumbered prefix fails. */
|
|
62
|
+
export declare function verifyCommitment(records: ChainRecordLike[], commitment: {
|
|
63
|
+
seq: number;
|
|
64
|
+
acc: Hex | string;
|
|
65
|
+
head?: Hex | string;
|
|
66
|
+
}): CommitmentVerifyResult;
|
package/dist/canonical.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { keccak256, toBytes } from 'viem';
|
|
1
|
+
import { concatHex, keccak256, numberToHex, stringToHex, toBytes } from 'viem';
|
|
2
2
|
/** Deterministic JSON — RFC 8785 (JCS) conformant for JSON-domain inputs: keys sorted by
|
|
3
3
|
* UTF-16 code units, no whitespace, JSON.stringify scalar/escape semantics. Arrays keep
|
|
4
4
|
* order; undefined properties are dropped (matches JSON.stringify semantics).
|
|
@@ -33,3 +33,76 @@ function serialize(value) {
|
|
|
33
33
|
export function digestOf(value) {
|
|
34
34
|
return keccak256(toBytes(canonicalStringify(value)));
|
|
35
35
|
}
|
|
36
|
+
/** Hash-chain link, byte-identical to the hosted ledger's recompute: the ledger
|
|
37
|
+
* counter-signs `keccak256(artifactDigest || prevDigest-or-32-zero-bytes || uint64be(seq))`.
|
|
38
|
+
* `prevDigest` is the PREVIOUS record's artifactDigest (null at seq 1). */
|
|
39
|
+
export const GENESIS_DIGEST = `0x${'0'.repeat(64)}`;
|
|
40
|
+
export function chainLinkDigest(artifactDigest, prevDigest, seq) {
|
|
41
|
+
return keccak256(concatHex([artifactDigest, prevDigest ?? GENESIS_DIGEST, numberToHex(seq, { size: 8 })]));
|
|
42
|
+
}
|
|
43
|
+
/** Chain commitment accumulator (anchored since 2026-08-28). DERIVED, never signed, never
|
|
44
|
+
* stored per row: acc_0 = keccak256(utf8("tersign-chain-commitment-v1")); acc_k =
|
|
45
|
+
* keccak256(acc_{k-1} || link_k). A pure function of (a_1..a_k, 1..k) — any omission,
|
|
46
|
+
* insertion, reordering or rewrite below k changes acc_k, so ONE anchored digest over acc_N
|
|
47
|
+
* commits to the whole prefix. The anchor stamps digestOf({acc, head, schema, seq}).
|
|
48
|
+
* Pinned cross-implementation (this SDK, the hosted ledger, the Python SDK and the evidence-bundle
|
|
49
|
+
* verifier share the same test vectors): edit all or none. */
|
|
50
|
+
export const CHAIN_COMMITMENT_SCHEMA = 'tersign-chain-commitment-v1';
|
|
51
|
+
export const ACC_GENESIS = keccak256(stringToHex(CHAIN_COMMITMENT_SCHEMA));
|
|
52
|
+
export function chainAccumulatorStep(acc, link) {
|
|
53
|
+
return keccak256(concatHex([acc, link]));
|
|
54
|
+
}
|
|
55
|
+
export function chainCommitment(seq, head, acc) {
|
|
56
|
+
return { acc, head, schema: CHAIN_COMMITMENT_SCHEMA, seq };
|
|
57
|
+
}
|
|
58
|
+
/** keccak256 over the JCS bytes of the commitment object — the anchored subject digest. */
|
|
59
|
+
export function commitmentDigest(seq, head, acc) {
|
|
60
|
+
return digestOf(chainCommitment(seq, head, acc));
|
|
61
|
+
}
|
|
62
|
+
/** A set of records that does not re-walk (gap, prev ≠ previous artifact, wrong seq): no
|
|
63
|
+
* accumulator is derived over it — a value no independent recompute would reproduce. */
|
|
64
|
+
export class ChainIntegrityError extends Error {
|
|
65
|
+
}
|
|
66
|
+
/** Fold records 1..N (in seq order, dense, prev-continuous). Returns acc_N and the head
|
|
67
|
+
* artifact digest a_N. Throws ChainIntegrityError on any discontinuity or on an empty set. */
|
|
68
|
+
export function foldAccumulator(records) {
|
|
69
|
+
if (records.length < 1)
|
|
70
|
+
throw new ChainIntegrityError('no records to fold');
|
|
71
|
+
let acc = ACC_GENESIS;
|
|
72
|
+
let prev = null;
|
|
73
|
+
for (let i = 0; i < records.length; i++) {
|
|
74
|
+
const r = records[i];
|
|
75
|
+
if (r.seq !== i + 1)
|
|
76
|
+
throw new ChainIntegrityError(`seq ${r.seq} at position ${i + 1}`);
|
|
77
|
+
if ((r.prevDigest ?? null) !== prev)
|
|
78
|
+
throw new ChainIntegrityError(`prev mismatch at seq ${r.seq}`);
|
|
79
|
+
acc = chainAccumulatorStep(acc, chainLinkDigest(r.artifactDigest, prev, r.seq));
|
|
80
|
+
prev = r.artifactDigest;
|
|
81
|
+
}
|
|
82
|
+
return { acc, head: prev };
|
|
83
|
+
}
|
|
84
|
+
/** Recompute the accumulator over `records` (seq 1..N as served by the ledger) and compare it
|
|
85
|
+
* to an anchored commitment (the `commitment` object from `/v1/receipts/{digest}/verify`, or
|
|
86
|
+
* any `{seq, acc, head?}`). `acc`/`digest` are the RECOMPUTED values — compare `digest` to the
|
|
87
|
+
* anchor's subjectDigest. A truncated, substituted, reordered or renumbered prefix fails. */
|
|
88
|
+
export function verifyCommitment(records, commitment) {
|
|
89
|
+
let folded;
|
|
90
|
+
try {
|
|
91
|
+
folded = foldAccumulator(records);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const reason = err instanceof ChainIntegrityError ? err.message : String(err);
|
|
95
|
+
return { ok: false, acc: ACC_GENESIS, digest: GENESIS_DIGEST, reason: `records do not re-walk: ${reason}` };
|
|
96
|
+
}
|
|
97
|
+
const digest = commitmentDigest(records.length, folded.head, folded.acc);
|
|
98
|
+
if (records.length !== commitment.seq) {
|
|
99
|
+
return { ok: false, acc: folded.acc, digest, reason: `records cover seq ≤ ${records.length}, commitment covers seq ≤ ${commitment.seq}` };
|
|
100
|
+
}
|
|
101
|
+
if (folded.acc.toLowerCase() !== String(commitment.acc).toLowerCase()) {
|
|
102
|
+
return { ok: false, acc: folded.acc, digest, reason: 'accumulator mismatch: the commitment was not built over this prefix' };
|
|
103
|
+
}
|
|
104
|
+
if (commitment.head !== undefined && folded.head.toLowerCase() !== String(commitment.head).toLowerCase()) {
|
|
105
|
+
return { ok: false, acc: folded.acc, digest, reason: 'head mismatch: the commitment names a different head record' };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, acc: folded.acc, digest };
|
|
108
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export { canonicalStringify, digestOf } from './canonical.js';
|
|
2
|
+
export { canonicalStringify, digestOf, GENESIS_DIGEST, chainLinkDigest, CHAIN_COMMITMENT_SCHEMA, ACC_GENESIS, chainAccumulatorStep, chainCommitment, commitmentDigest, foldAccumulator, verifyCommitment, ChainIntegrityError, type ChainCommitment, type ChainRecordLike, type CommitmentVerifyResult, } from './canonical.js';
|
|
3
3
|
export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, type VerifyResult, } from './receipt/eip712.js';
|
|
4
4
|
export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, COMPLIANCE_WIRE_VECTOR, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, type IssuerConfig, type MinimalRecordInput, } from './compliance/record.js';
|
|
5
5
|
export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, type IdempotencyStore, type IdempotencyOutcome, type IdempotencyOptions, type CachedResponse, type FingerprintParts, } from './idempotency/middleware.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export { canonicalStringify, digestOf } from './canonical.js';
|
|
2
|
+
export { canonicalStringify, digestOf, GENESIS_DIGEST, chainLinkDigest, CHAIN_COMMITMENT_SCHEMA, ACC_GENESIS, chainAccumulatorStep, chainCommitment, commitmentDigest, foldAccumulator, verifyCommitment, ChainIntegrityError, } from './canonical.js';
|
|
3
3
|
export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, } from './receipt/eip712.js';
|
|
4
4
|
export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, COMPLIANCE_WIRE_VECTOR, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, } from './compliance/record.js';
|
|
5
5
|
export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, } from './idempotency/middleware.js';
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export declare function envDeps(env?: Record<string, string | undefined>): McpDe
|
|
|
8
8
|
* every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
|
|
9
9
|
export declare const MCP_SERVER_IDENTITY: {
|
|
10
10
|
readonly name: "tersign";
|
|
11
|
-
readonly version: "0.4.
|
|
11
|
+
readonly version: "0.4.5";
|
|
12
12
|
};
|
|
13
13
|
export declare function buildServer(deps: McpDeps): McpServer;
|
package/dist/mcp/server.js
CHANGED
|
@@ -45,30 +45,47 @@ function json(value) {
|
|
|
45
45
|
}
|
|
46
46
|
/** MUST match package.json name/version — the MCP handshake self-reports this identity to
|
|
47
47
|
* every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
|
|
48
|
-
export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.4.
|
|
48
|
+
export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.4.5' };
|
|
49
49
|
export function buildServer(deps) {
|
|
50
50
|
const server = new McpServer(MCP_SERVER_IDENTITY);
|
|
51
51
|
server.registerTool('issue_receipt', {
|
|
52
52
|
title: 'Issue signed receipt',
|
|
53
|
-
description: 'Issue an x402 offer-receipt (EIP-712) plus
|
|
53
|
+
description: 'Issue an x402 offer-receipt (EIP-712) plus a Tersign action record for a payment that has ALREADY settled, and counter-sign both into your hash chain when a ledger is configured. ' +
|
|
54
|
+
'Use this for money that moved; use record_disclosure for a non-payment agent action. ' +
|
|
55
|
+
'Side effects: signs with TERSIGN_SELLER_KEY, and performs ONE network write to the ledger when TERSIGN_LEDGER_URL/_API_KEY/_SELLER_ID are set (without them it signs locally and returns an unchained artifact). ' +
|
|
56
|
+
'Returns the signed receipt artifact, its keccak256 canonical digest, and — when chained — the ledger counter-signature and sequence number.',
|
|
54
57
|
inputSchema: {
|
|
55
|
-
network: z.string().describe('CAIP-2, e.g. eip155:8453'),
|
|
56
|
-
resourceUrl: z.string().url(),
|
|
57
|
-
payer: z.string(),
|
|
58
|
-
supplyDescription: z.string(),
|
|
59
|
-
settledAt: z.number().int().optional(),
|
|
60
|
-
txHash: z.string().optional(),
|
|
61
|
-
taxScheme: z
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
network: z.string().describe('settlement network as CAIP-2, e.g. "eip155:8453" for Base mainnet'),
|
|
59
|
+
resourceUrl: z.string().url().describe('absolute URL of the resource that was paid for; appears verbatim in the receipt'),
|
|
60
|
+
payer: z.string().describe('0x address that paid — the party who can later open a dispute against this receipt'),
|
|
61
|
+
supplyDescription: z.string().describe('what was supplied, in the seller\'s own words; the human-readable line an auditor or venue reads'),
|
|
62
|
+
settledAt: z.number().int().optional().describe('unix seconds when settlement occurred; defaults to now. Set it explicitly when back-filling'),
|
|
63
|
+
txHash: z.string().optional().describe('on-chain settlement transaction hash, when one exists; omit for off-chain or fiat settlement'),
|
|
64
|
+
taxScheme: z
|
|
65
|
+
.enum(['none', 'vat', 'gst', 'jct', 'sales'])
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('tax regime the seller is accounting under; recorded, never computed — Tersign does not calculate tax'),
|
|
68
|
+
currency: z.string().optional().describe('settlement currency code, e.g. "USDC" or "USD"'),
|
|
69
|
+
principal: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('the party on whose authority the paying agent acted (x402 sense: the buyer who delegated). Omit when a human paid directly'),
|
|
64
73
|
},
|
|
65
74
|
}, async (args) => json(await issueReceiptTool(deps, args)));
|
|
66
75
|
server.registerTool('verify_receipt', {
|
|
67
76
|
title: 'Verify signed receipt',
|
|
68
|
-
description: 'Verify an offer-receipt artifact
|
|
77
|
+
description: 'Verify an offer-receipt artifact: recover the EIP-712 signature and confirm the payload digest binds to it. ' +
|
|
78
|
+
'Fully OFFLINE — no network, no API key, no account; verifying someone else\'s receipt is the intended use. ' +
|
|
79
|
+
'Use this for a receipt (money); use verify_compliance_record for an action record (a non-payment action). ' +
|
|
80
|
+
'Returns { valid, signer, digest } and, when expectedSigner is supplied and does not match, valid:false with the recovered signer so you can see who actually signed.',
|
|
69
81
|
inputSchema: {
|
|
70
|
-
artifact: z
|
|
71
|
-
|
|
82
|
+
artifact: z
|
|
83
|
+
.record(z.unknown())
|
|
84
|
+
.describe('the receipt artifact exactly as issued: { format, payload, signature }. Pass the object, not a JSON string'),
|
|
85
|
+
expectedSigner: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('0x address the receipt MUST be signed by — obtain it out-of-band, never from the artifact. Omit to recover the signer without enforcing it'),
|
|
72
89
|
},
|
|
73
90
|
}, async ({ artifact, expectedSigner }) => json(await verifyReceiptTool(artifact, expectedSigner)));
|
|
74
91
|
server.registerTool('record_disclosure', {
|
|
@@ -78,27 +95,49 @@ export function buildServer(deps) {
|
|
|
78
95
|
text: z.string().optional().describe('the disclosure text as presented — digested locally, never transmitted'),
|
|
79
96
|
textDigest: z.string().regex(/^0x[0-9a-fA-F]{64}$/).optional().describe('pre-computed digest (wins over text)'),
|
|
80
97
|
medium: z.string().optional().describe("channel: 'chat' | 'api' | 'voice' | 'ui' …"),
|
|
81
|
-
kind: z
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
kind: z
|
|
99
|
+
.enum(['ai-interaction', 'synthetic-content'])
|
|
100
|
+
.optional()
|
|
101
|
+
.describe("what was disclosed: 'ai-interaction' = the user was told they are talking to an AI; 'synthetic-content' = output was marked machine-generated. Defaults to 'ai-interaction'"),
|
|
102
|
+
agentId: z.string().describe('stable identifier for the disclosing agent — keep it constant across calls so one chain accumulates per agent'),
|
|
103
|
+
resourceUrl: z.string().url().optional().describe('absolute URL of the surface the disclosure was presented on, when there is one'),
|
|
84
104
|
},
|
|
85
105
|
}, async (args) => json(await recordDisclosureTool(deps, args)));
|
|
86
106
|
server.registerTool('verify_compliance_record', {
|
|
87
107
|
title: 'Verify compliance record',
|
|
88
|
-
description: 'Verify
|
|
108
|
+
description: 'Verify a Tersign action record against its attestation: recompute the record\'s canonical digest, confirm the attestation commits to that exact digest, and recover the signature. ' +
|
|
109
|
+
'Fully OFFLINE — no network, no API key, no account. ' +
|
|
110
|
+
'Use this for an action record (a disclosure or other non-payment agent action); use verify_receipt for a payment receipt. ' +
|
|
111
|
+
'PASS proves integrity and internal consistency only. Authorship needs an out-of-band signer address: pass expectedSigner, or the identity is whatever the artifact claims about itself. ' +
|
|
112
|
+
'Returns { valid, signer, digest }; on mismatch, valid:false plus the recovered signer and the recomputed digest.',
|
|
89
113
|
inputSchema: {
|
|
90
|
-
record: z
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
record: z
|
|
115
|
+
.record(z.unknown())
|
|
116
|
+
.describe('the action record object as issued (ComplianceRecordV1 shape). Pass the object, not a JSON string; any field edit changes the digest and fails verification — which is the point'),
|
|
117
|
+
attestation: z
|
|
118
|
+
.record(z.unknown())
|
|
119
|
+
.describe('the attestation that accompanies the record: the signature over the record digest, as returned alongside it at issuance'),
|
|
120
|
+
expectedSigner: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('0x address the record MUST be signed by, obtained out-of-band (for the public ledger: https://tersign.ai/v1/ledger). Omit to recover the signer without enforcing it'),
|
|
93
124
|
},
|
|
94
125
|
}, async ({ record, attestation, expectedSigner }) => json(await verifyRecordTool(record, attestation, expectedSigner)));
|
|
95
126
|
server.registerTool('record_refund', {
|
|
96
127
|
title: 'Record refund',
|
|
97
|
-
description: 'Record a refund against
|
|
128
|
+
description: 'Record a refund against an already-chained receipt, as the SELLER. The refund becomes its own counter-signed entry that references the original — nothing is edited or deleted, so the chain stays append-only and both the charge and the refund remain visible. ' +
|
|
129
|
+
'Requires ledger configuration (TERSIGN_LEDGER_URL/_API_KEY/_SELLER_ID) and performs one network write; errors if the original digest is not on your chain. ' +
|
|
130
|
+
'This RECORDS a refund you have already made — it moves no money. ' +
|
|
131
|
+
'Returns the refund record, its digest, the ledger counter-signature and sequence number.',
|
|
98
132
|
inputSchema: {
|
|
99
|
-
originalDigest: z
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
originalDigest: z
|
|
134
|
+
.string()
|
|
135
|
+
.regex(/^0x[0-9a-fA-F]{64}$/)
|
|
136
|
+
.describe('0x-prefixed keccak256 digest of the receipt being refunded — the digest returned by issue_receipt, and it must already exist on your chain'),
|
|
137
|
+
amount: z
|
|
138
|
+
.string()
|
|
139
|
+
.describe('refunded amount as a decimal STRING in the original settlement currency, e.g. "12.50". A string, not a number, so no precision is lost. Partial refunds are allowed'),
|
|
140
|
+
reason: z.string().describe('why the refund was issued, in your own words; recorded verbatim for whoever reads the chain later'),
|
|
102
141
|
},
|
|
103
142
|
}, async ({ originalDigest, amount, reason }) => json(await recordRefundTool(deps, originalDigest, amount, reason)));
|
|
104
143
|
const digestSchema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
|
|
@@ -106,9 +145,13 @@ export function buildServer(deps) {
|
|
|
106
145
|
title: 'Open dispute',
|
|
107
146
|
description: 'Open an objective dispute against a counter-signed receipt as the PAYER (the configured key must be the receipt payer). Reasons: not_delivered, wrong_content, duplicate_charge. Contested non-mechanical claims escalate to the arbiter; duplicate_charge is decided instantly from ledger arithmetic.',
|
|
108
147
|
inputSchema: {
|
|
109
|
-
receiptDigest: digestSchema,
|
|
110
|
-
reason: z
|
|
111
|
-
|
|
148
|
+
receiptDigest: digestSchema.describe('0x-prefixed keccak256 digest of the counter-signed receipt being disputed'),
|
|
149
|
+
reason: z
|
|
150
|
+
.enum(['not_delivered', 'wrong_content', 'duplicate_charge'])
|
|
151
|
+
.describe("grounds: 'not_delivered' nothing arrived · 'wrong_content' delivered but not what was bought · 'duplicate_charge' the same supply was billed twice (decided mechanically from the chain, no arbiter)"),
|
|
152
|
+
claimAmount: z
|
|
153
|
+
.string()
|
|
154
|
+
.describe('amount claimed back, as a decimal STRING in the receipt\'s settlement currency, e.g. "12.50"; must not exceed the receipt amount'),
|
|
112
155
|
statement: z.string().optional().describe('for humans reading the record — never an adjudication input'),
|
|
113
156
|
},
|
|
114
157
|
}, async ({ receiptDigest, reason, claimAmount, statement }) => json(await openDisputeTool(deps, { receiptDigest: receiptDigest, reason, claimAmount, statement })));
|
|
@@ -116,16 +159,21 @@ export function buildServer(deps) {
|
|
|
116
159
|
title: 'Submit dispute evidence',
|
|
117
160
|
description: 'Submit signed evidence to an open dispute. Claimant evidence must be signed by the payer key; respondent evidence additionally requires the seller API key (TERSIGN_LEDGER_API_KEY).',
|
|
118
161
|
inputSchema: {
|
|
119
|
-
disputeDigest: digestSchema,
|
|
120
|
-
role: z
|
|
162
|
+
disputeDigest: digestSchema.describe('0x-prefixed digest of the open dispute, as returned by open_dispute'),
|
|
163
|
+
role: z
|
|
164
|
+
.enum(['claimant', 'respondent'])
|
|
165
|
+
.describe("which side you are filing as: 'claimant' = the payer who opened it (payer key) · 'respondent' = the seller answering it (also needs TERSIGN_LEDGER_API_KEY)"),
|
|
121
166
|
artifacts: z
|
|
122
167
|
.array(z.object({
|
|
123
|
-
kind: z
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
kind: z
|
|
169
|
+
.enum(['content-digest', 'delivery-attestation', 'payment-proof', 'transcript'])
|
|
170
|
+
.describe('what this artifact is; the adjudicator treats each kind differently'),
|
|
171
|
+
digest: digestSchema.describe('0x-prefixed keccak256 digest of the artifact. Only the DIGEST is submitted — the content itself never leaves your side'),
|
|
172
|
+
at: z.number().int().optional().describe('unix seconds the artifact was produced; supply it when timing is part of your argument'),
|
|
173
|
+
note: z.string().optional().describe('short human-readable label for whoever reads the record; never an adjudication input'),
|
|
127
174
|
}))
|
|
128
|
-
.min(1)
|
|
175
|
+
.min(1)
|
|
176
|
+
.describe('at least one evidence reference; submit every artifact you want considered in a single call'),
|
|
129
177
|
},
|
|
130
178
|
}, async ({ disputeDigest, role, artifacts }) => json(await submitEvidenceTool(deps, {
|
|
131
179
|
disputeDigest: disputeDigest,
|
|
@@ -134,13 +182,20 @@ export function buildServer(deps) {
|
|
|
134
182
|
})));
|
|
135
183
|
server.registerTool('adjudicate_dispute', {
|
|
136
184
|
title: 'Adjudicate dispute',
|
|
137
|
-
description: 'Trigger deterministic adjudication of an open dispute
|
|
138
|
-
|
|
185
|
+
description: 'Trigger deterministic adjudication of an open dispute. The v0 rulebook is public and the verdict is recomputable by anyone from the chain — no discretion, no model in the loop. ' +
|
|
186
|
+
'Side effects: writes a verdict entry, and a refund verdict automatically creates the corresponding refund record. Adjudicating twice is not meaningful; the first verdict stands. ' +
|
|
187
|
+
'Returns the verdict, the rationale naming the rule applied, and the ledger signature over both.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
disputeDigest: digestSchema.describe('0x-prefixed digest of the open dispute to adjudicate, as returned by open_dispute'),
|
|
190
|
+
},
|
|
139
191
|
}, async ({ disputeDigest }) => json(await adjudicateDisputeTool(deps, disputeDigest)));
|
|
140
192
|
server.registerTool('get_dispute', {
|
|
141
193
|
title: 'Get dispute record',
|
|
142
|
-
description: 'Fetch a dispute
|
|
143
|
-
|
|
194
|
+
description: 'Fetch a dispute in full: its state, both sides\' evidence references, the verdict and rationale once adjudicated, and the ledger signature over the record. ' +
|
|
195
|
+
'Read-only — one network read, no key required, and safe to poll while a dispute is open.',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
disputeDigest: digestSchema.describe('0x-prefixed digest of the dispute to fetch, as returned by open_dispute'),
|
|
198
|
+
},
|
|
144
199
|
}, async ({ disputeDigest }) => json(await getDisputeTool(deps, disputeDigest)));
|
|
145
200
|
return server;
|
|
146
201
|
}
|
package/dist/verify-bin.js
CHANGED
|
@@ -48,6 +48,13 @@ async function checkLedger(digest, url) {
|
|
|
48
48
|
fail('ledger record found but the counter-signed hash-chain does NOT verify');
|
|
49
49
|
console.log(`ledger: ${url}`);
|
|
50
50
|
console.log(` counter-signed OK (seller ${body.sellerId}, seq ${body.seq}, ledger key ${body.ledgerSigner})`);
|
|
51
|
+
// Present once the record sits under an anchored chain commitment (anchors since 2026-08-28):
|
|
52
|
+
// the accumulator covers every seq ≤ commitment.seq, so the anchor binds this record too.
|
|
53
|
+
const c = body.commitment;
|
|
54
|
+
if (c) {
|
|
55
|
+
const block = c.bitcoinBlockHeight ? ` block ${c.bitcoinBlockHeight}` : '';
|
|
56
|
+
console.log(` commitment: seq ≤ ${c.seq} committed (acc ${c.acc.slice(0, 10)}…) — ${c.status}${block}`);
|
|
57
|
+
}
|
|
51
58
|
}
|
|
52
59
|
if (/^0x[0-9a-f]{64}$/i.test(target)) {
|
|
53
60
|
await checkLedger(target, ledger ?? DEFAULT_LEDGER);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tersign",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Tersign \u2014 the evidence layer for the agent economy. Counter-signed receipts, agent action records, idempotency enforcement, refunds, disputes, and jury-ready evidence envelopes for x402/agent-commerce sellers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|