tersign 0.4.3 → 0.4.4
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 +1 -1
- 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.4";
|
|
12
12
|
};
|
|
13
13
|
export declare function buildServer(deps: McpDeps): McpServer;
|
package/dist/mcp/server.js
CHANGED
|
@@ -45,7 +45,7 @@ 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.4' };
|
|
49
49
|
export function buildServer(deps) {
|
|
50
50
|
const server = new McpServer(MCP_SERVER_IDENTITY);
|
|
51
51
|
server.registerTool('issue_receipt', {
|
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.4",
|
|
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",
|