tersign 0.5.0 → 0.6.1

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/dist/assure.d.ts CHANGED
@@ -9,7 +9,8 @@ export interface AssureConfig {
9
9
  ledger?: LedgerConfig;
10
10
  }
11
11
  export interface SettlementContext {
12
- /** CAIP-2 */
12
+ /** CAIP-2 (`"eip155:8453"`), or an x402 v1 name (`"base"`), which the receipt payload carries as
13
+ * CAIP-2; an unknown name throws. */
13
14
  network: string;
14
15
  resourceUrl: string;
15
16
  payer: string;
@@ -30,18 +31,79 @@ export interface IssuedReceipt {
30
31
  compliance: SignedComplianceRecord;
31
32
  ledger?: CountersignResult;
32
33
  }
33
- /** The core primitive: after a settled x402 payment, issue the signed base receipt
34
- * (merged offer-receipt extension, EIP-712) plus the Tersign compliance record bound to it,
35
- * and counter-sign into the hosted ledger when configured. Attach the result to the
36
- * SettlementResponse via `attachToExtensions`. */
34
+ /** The core primitive: after a settled x402 payment, issue the seller-signed receipt
35
+ * (offer-receipt extension, EIP-712) and the Tersign compliance record. With a ledger configured,
36
+ * the ledger counter-signs the receipt into the seller's hash chain; the compliance-fields record is not
37
+ * counter-signed, and is bound to its receipt only by the seller's own signature, which covers the
38
+ * receipt's digest. Merge the result into the x402 SettlementResponse (the `PAYMENT-RESPONSE`
39
+ * header) with `attachToSettlementResponse`; `withAssure` does this for you. */
37
40
  export declare class Assure {
38
41
  private cfg;
39
42
  private ledger?;
40
43
  constructor(cfg: AssureConfig);
41
44
  issueFor(ctx: SettlementContext): Promise<IssuedReceipt>;
42
45
  }
43
- /** Decorate an x402 SettlementResponse body with the receipt at the spec-defined placement
44
- * (`extensions["offer-receipt"].info.receipt`) and the Tersign record alongside it. */
46
+ /** JSON Schema for `extensions["offer-receipt"]` on a SettlementResponse — the same shape the
47
+ * upstream reference server attaches (x402-foundation/x402 typescript/packages/extensions/src/
48
+ * offer-receipt/server.ts RECEIPT_SCHEMA, main 5eee1e3c35, read 2026-09-29). */
49
+ export declare const OFFER_RECEIPT_RESPONSE_SCHEMA: {
50
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
51
+ readonly type: "object";
52
+ readonly properties: {
53
+ readonly receipt: {
54
+ readonly type: "object";
55
+ readonly properties: {
56
+ readonly format: {
57
+ readonly type: "string";
58
+ };
59
+ readonly payload: {
60
+ readonly type: "object";
61
+ readonly properties: {
62
+ readonly version: {
63
+ readonly type: "integer";
64
+ };
65
+ readonly network: {
66
+ readonly type: "string";
67
+ };
68
+ readonly resourceUrl: {
69
+ readonly type: "string";
70
+ };
71
+ readonly payer: {
72
+ readonly type: "string";
73
+ };
74
+ readonly issuedAt: {
75
+ readonly type: "integer";
76
+ };
77
+ readonly transaction: {
78
+ readonly type: "string";
79
+ };
80
+ };
81
+ readonly required: readonly ["version", "network", "resourceUrl", "payer", "issuedAt"];
82
+ };
83
+ readonly signature: {
84
+ readonly type: "string";
85
+ };
86
+ };
87
+ readonly required: readonly ["format", "signature"];
88
+ };
89
+ };
90
+ readonly required: readonly ["receipt"];
91
+ };
92
+ /** Merge the receipt and the compliance record into an x402 `SettlementResponse` — the object
93
+ * the HTTP transport carries, base64-encoded, in the `PAYMENT-RESPONSE` header. The receipt goes
94
+ * to `extensions["offer-receipt"].info.receipt` (offer-receipt spec §5.1, same for v1 and v2)
95
+ * with its schema; the record and its attestation to `extensions["compliance-fields"].info`
96
+ * (placement proposed in x402-foundation/x402#2853, open). Every other member of the settlement
97
+ * response, and every other extension, is kept; an `offer-receipt` entry already present is
98
+ * replaced, because the record binds to THIS receipt's digest. */
99
+ export declare function attachToSettlementResponse<T extends Record<string, unknown>>(settlement: T, issued: IssuedReceipt): T & {
100
+ extensions: Record<string, unknown>;
101
+ };
102
+ /** LEGACY: decorate the resource's JSON response BODY with the same extension entries. The x402
103
+ * v2 HTTP transport carries protocol data in headers only ("Response bodies are a server
104
+ * implementation concern"), so a wallet reading `PAYMENT-RESPONSE` never sees a body placement.
105
+ * Kept for readers built against tersign ≤0.5; `withAssure` uses it only with
106
+ * `legacyBodyPlacement: true`. */
45
107
  export declare function attachToExtensions<T extends Record<string, unknown>>(responseBody: T, issued: IssuedReceipt): T & {
46
108
  extensions: Record<string, unknown>;
47
109
  };
package/dist/assure.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { signReceipt } from './receipt/eip712.js';
2
+ import { toCaip2Network } from './receipt/network.js';
2
3
  import { buildMinimalRecord, signComplianceRecord } from './compliance/record.js';
3
4
  import { LedgerClient } from './ledgerClient.js';
4
- /** The core primitive: after a settled x402 payment, issue the signed base receipt
5
- * (merged offer-receipt extension, EIP-712) plus the Tersign compliance record bound to it,
6
- * and counter-sign into the hosted ledger when configured. Attach the result to the
7
- * SettlementResponse via `attachToExtensions`. */
5
+ /** The core primitive: after a settled x402 payment, issue the seller-signed receipt
6
+ * (offer-receipt extension, EIP-712) and the Tersign compliance record. With a ledger configured,
7
+ * the ledger counter-signs the receipt into the seller's hash chain; the compliance-fields record is not
8
+ * counter-signed, and is bound to its receipt only by the seller's own signature, which covers the
9
+ * receipt's digest. Merge the result into the x402 SettlementResponse (the `PAYMENT-RESPONSE`
10
+ * header) with `attachToSettlementResponse`; `withAssure` does this for you. */
8
11
  export class Assure {
9
12
  cfg;
10
13
  ledger;
@@ -16,7 +19,7 @@ export class Assure {
16
19
  async issueFor(ctx) {
17
20
  const payload = {
18
21
  version: 1,
19
- network: ctx.network,
22
+ network: toCaip2Network(ctx.network),
20
23
  resourceUrl: ctx.resourceUrl,
21
24
  payer: ctx.payer,
22
25
  issuedAt: ctx.settledAt,
@@ -45,8 +48,71 @@ export class Assure {
45
48
  return { receipt, compliance, ledger };
46
49
  }
47
50
  }
48
- /** Decorate an x402 SettlementResponse body with the receipt at the spec-defined placement
49
- * (`extensions["offer-receipt"].info.receipt`) and the Tersign record alongside it. */
51
+ /** JSON Schema for `extensions["offer-receipt"]` on a SettlementResponse — the same shape the
52
+ * upstream reference server attaches (x402-foundation/x402 typescript/packages/extensions/src/
53
+ * offer-receipt/server.ts RECEIPT_SCHEMA, main 5eee1e3c35, read 2026-09-29). */
54
+ export const OFFER_RECEIPT_RESPONSE_SCHEMA = {
55
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
56
+ type: 'object',
57
+ properties: {
58
+ receipt: {
59
+ type: 'object',
60
+ properties: {
61
+ format: { type: 'string' },
62
+ payload: {
63
+ type: 'object',
64
+ properties: {
65
+ version: { type: 'integer' },
66
+ network: { type: 'string' },
67
+ resourceUrl: { type: 'string' },
68
+ payer: { type: 'string' },
69
+ issuedAt: { type: 'integer' },
70
+ transaction: { type: 'string' },
71
+ },
72
+ required: ['version', 'network', 'resourceUrl', 'payer', 'issuedAt'],
73
+ },
74
+ signature: { type: 'string' },
75
+ },
76
+ required: ['format', 'signature'],
77
+ },
78
+ },
79
+ required: ['receipt'],
80
+ };
81
+ function complianceInfo(issued) {
82
+ return {
83
+ record: issued.compliance.record,
84
+ attestation: issued.compliance.attestation,
85
+ ...(issued.ledger
86
+ ? { ledger: { seq: issued.ledger.seq, digest: issued.ledger.digest, countersignature: issued.ledger.countersignature } }
87
+ : {}),
88
+ };
89
+ }
90
+ /** Merge the receipt and the compliance record into an x402 `SettlementResponse` — the object
91
+ * the HTTP transport carries, base64-encoded, in the `PAYMENT-RESPONSE` header. The receipt goes
92
+ * to `extensions["offer-receipt"].info.receipt` (offer-receipt spec §5.1, same for v1 and v2)
93
+ * with its schema; the record and its attestation to `extensions["compliance-fields"].info`
94
+ * (placement proposed in x402-foundation/x402#2853, open). Every other member of the settlement
95
+ * response, and every other extension, is kept; an `offer-receipt` entry already present is
96
+ * replaced, because the record binds to THIS receipt's digest. */
97
+ export function attachToSettlementResponse(settlement, issued) {
98
+ const prior = isPlainRecord(settlement.extensions) ? settlement.extensions : {};
99
+ return {
100
+ ...settlement,
101
+ extensions: {
102
+ ...prior,
103
+ 'offer-receipt': { info: { receipt: issued.receipt }, schema: OFFER_RECEIPT_RESPONSE_SCHEMA },
104
+ 'compliance-fields': { info: complianceInfo(issued) },
105
+ },
106
+ };
107
+ }
108
+ function isPlainRecord(v) {
109
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
110
+ }
111
+ /** LEGACY: decorate the resource's JSON response BODY with the same extension entries. The x402
112
+ * v2 HTTP transport carries protocol data in headers only ("Response bodies are a server
113
+ * implementation concern"), so a wallet reading `PAYMENT-RESPONSE` never sees a body placement.
114
+ * Kept for readers built against tersign ≤0.5; `withAssure` uses it only with
115
+ * `legacyBodyPlacement: true`. */
50
116
  export function attachToExtensions(responseBody, issued) {
51
117
  const prior = (responseBody.extensions ?? {});
52
118
  return {
@@ -54,15 +120,7 @@ export function attachToExtensions(responseBody, issued) {
54
120
  extensions: {
55
121
  ...prior,
56
122
  'offer-receipt': { info: { receipt: issued.receipt } },
57
- 'compliance-fields': {
58
- info: {
59
- record: issued.compliance.record,
60
- attestation: issued.compliance.attestation,
61
- ...(issued.ledger
62
- ? { ledger: { seq: issued.ledger.seq, digest: issued.ledger.digest, countersignature: issued.ledger.countersignature } }
63
- : {}),
64
- },
65
- },
123
+ 'compliance-fields': { info: complianceInfo(issued) },
66
124
  },
67
125
  };
68
126
  }
@@ -1,8 +1,8 @@
1
1
  import type { Account } from 'viem/accounts';
2
2
  import type { Adjustment, ComplianceRecordV1, SignedComplianceRecord, SignedReceipt, VerifyLike } from './types.js';
3
3
  import { type SignerBinding } from '../receipt/binding.js';
4
- /** Canonical domain per the compliance-fields extension spec (x402-foundation/x402#2853).
5
- * Migrated from the vendor domain 'tersign compliance-record' on 2026-07-14 while ZERO
4
+ /** EIP-712 domain named in the proposed compliance-fields extension (x402-foundation/x402#2853,
5
+ * open — proposed, not settled spec). Migrated from the vendor domain 'tersign compliance-record' on 2026-07-14 while ZERO
6
6
  * production compliance records existed — a free change then, a breaking wire change after. */
7
7
  export declare const COMPLIANCE_DOMAIN: {
8
8
  readonly name: "compliance-fields";
@@ -32,7 +32,8 @@ export interface IssuerConfig {
32
32
  name: string;
33
33
  jurisdiction: string;
34
34
  taxId?: string;
35
- /** default 7 — HK IRO s.51C floor, ≥ MiCA 5+2 */
35
+ /** default 7 where no longer period applies (HK IRO s.51C, MiCA 5+2) — set the longest period
36
+ * that applies to you (e.g. DE § 14b UStG: eight years); 7 is a default, not a sufficiency claim */
36
37
  retentionYears?: number;
37
38
  }
38
39
  export interface MinimalRecordInput {
@@ -45,8 +46,13 @@ export interface MinimalRecordInput {
45
46
  refundOf?: `0x${string}`;
46
47
  adjustment?: Adjustment;
47
48
  }
48
- /** MINIMAL tier ≈ EU VAT Art 226b simplified-invoice content — legally sufficient for
49
- * sub-€100 supplies EU-wide, and the default for machine-to-machine micro-receipts. */
49
+ /** Build a record carrying the MINIMAL members of the proposed compliance-fields extension —
50
+ * content isomorphic to the EU VAT Art 226b simplified invoice. Whether it suffices as an invoice
51
+ * is decided by the invoicing rules that apply to the supply (Art 219a): Art 220a(1)(a) makes
52
+ * every Member State allow simplified invoices up to EUR 100, a Member State may require further
53
+ * details on them (Art 226b), and some supplies can never use one (Art 220(1)(2)-(3), Art 220a(2),
54
+ * and national bars). MINIMAL needs `tax.amount` whenever `tax.scheme` is not `none`; that is the caller's
55
+ * input. `seq` is not set here: it is the issuer's to assign before attestation. */
50
56
  export declare function buildMinimalRecord(issuer: IssuerConfig, input: MinimalRecordInput): ComplianceRecordV1;
51
57
  export declare function recordDigest(record: ComplianceRecordV1): `0x${string}`;
52
58
  export declare function signComplianceRecord(record: ComplianceRecordV1, account: Account): Promise<SignedComplianceRecord>;
@@ -1,8 +1,8 @@
1
1
  import { recoverTypedDataAddress } from 'viem';
2
2
  import { digestOf } from '../canonical.js';
3
3
  import { bindSigner, isPlainObject, parseExpectedSigner, signatureError } from '../receipt/binding.js';
4
- /** Canonical domain per the compliance-fields extension spec (x402-foundation/x402#2853).
5
- * Migrated from the vendor domain 'tersign compliance-record' on 2026-07-14 while ZERO
4
+ /** EIP-712 domain named in the proposed compliance-fields extension (x402-foundation/x402#2853,
5
+ * open — proposed, not settled spec). Migrated from the vendor domain 'tersign compliance-record' on 2026-07-14 while ZERO
6
6
  * production compliance records existed — a free change then, a breaking wire change after. */
7
7
  export const COMPLIANCE_DOMAIN = { name: 'compliance-fields', version: '1', chainId: 1n };
8
8
  export const COMPLIANCE_TYPES = {
@@ -20,11 +20,17 @@ export const COMPLIANCE_WIRE_VECTOR = digestOf({
20
20
  domain: { ...COMPLIANCE_DOMAIN, chainId: 1 },
21
21
  types: COMPLIANCE_TYPES,
22
22
  });
23
- /** MINIMAL tier ≈ EU VAT Art 226b simplified-invoice content — legally sufficient for
24
- * sub-€100 supplies EU-wide, and the default for machine-to-machine micro-receipts. */
23
+ /** Build a record carrying the MINIMAL members of the proposed compliance-fields extension —
24
+ * content isomorphic to the EU VAT Art 226b simplified invoice. Whether it suffices as an invoice
25
+ * is decided by the invoicing rules that apply to the supply (Art 219a): Art 220a(1)(a) makes
26
+ * every Member State allow simplified invoices up to EUR 100, a Member State may require further
27
+ * details on them (Art 226b), and some supplies can never use one (Art 220(1)(2)-(3), Art 220a(2),
28
+ * and national bars). MINIMAL needs `tax.amount` whenever `tax.scheme` is not `none`; that is the caller's
29
+ * input. `seq` is not set here: it is the issuer's to assign before attestation. */
25
30
  export function buildMinimalRecord(issuer, input) {
26
31
  const record = {
27
32
  version: 1,
33
+ canonicalizationVersion: 1,
28
34
  receiptDigest: digestOf(input.receipt),
29
35
  issuedAt: input.issuedAt,
30
36
  issuer: {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { canonicalStringify, digestOf, GENESIS_DIGEST, chainLinkDigest, CHAIN_CO
3
3
  export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, SIGNED_RECEIPT_FIELDS, type VerifyResult, } from './receipt/eip712.js';
4
4
  export { PUBLISHED_TEST_KEYS, publishedKeyLabel } from './receipt/known-keys.js';
5
5
  export { signerStatus, type SignerBinding, type SignerStatus } from './receipt/binding.js';
6
+ export { toCaip2Network } from './receipt/network.js';
6
7
  export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, COMPLIANCE_WIRE_VECTOR, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, type RecordVerifyResult, type IssuerConfig, type MinimalRecordInput, } from './compliance/record.js';
7
8
  export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, type IdempotencyStore, type IdempotencyOutcome, type IdempotencyOptions, type CachedResponse, type FingerprintParts, } from './idempotency/middleware.js';
8
9
  export { D1IdempotencyStore, D1_IDEMPOTENCY_DDL, type D1Like } from './idempotency/d1.js';
@@ -11,7 +12,7 @@ export { DISPUTE_DOMAIN, EVIDENCE_DOMAIN, CRITERIA_DOMAIN, DISPUTE_TYPES, EVIDEN
11
12
  export { ACTION_DOMAIN, ACTION_TYPES, ACTION_WIRE_VECTOR, actionDigest, signActionRecord, verifyActionRecord, type ActionKind, type DisclosureKind, type GovernanceOutcome, type ActionRecordV1, type ActionAttestationPayload, type SignedActionRecord, } from './evidence/action.js';
12
13
  export { recordDisclosure, type RecordDisclosureOptions, type RecordDisclosureResult } from './evidence/disclose.js';
13
14
  export { LedgerClient, type LedgerConfig, type CountersignResult } from './ledgerClient.js';
14
- export { Assure, attachToExtensions, type AssureConfig, type SettlementContext, type IssuedReceipt } from './assure.js';
15
- export { withAssure, extractSettlement, extractPaymentPayload, type WithAssureConfig, type SettlementInfo } from './adapter/x402.js';
15
+ export { Assure, attachToExtensions, attachToSettlementResponse, OFFER_RECEIPT_RESPONSE_SCHEMA, type AssureConfig, type SettlementContext, type IssuedReceipt, } from './assure.js';
16
+ export { withAssure, extractSettlement, extractPaymentPayload, encodeX402Header, decodeX402Header, COMPLIANCE_FIELDS_ADVERTISEMENT_SCHEMA, type WithAssureConfig, type SettlementInfo, type ComplianceAdvertisement, } from './adapter/x402.js';
16
17
  export { ENVELOPE_STATEMENT_MAX_CHARS, VENUE_SUBMISSION_MAX_CHARS, ENVELOPE_VENUES, type EvidenceEnvelopeV1, type EnvelopeSubjectKind, type EnvelopeVenue, } from './envelope/types.js';
17
18
  export { assertEnvelopeCaps, toInternetCourtSubmission, toKlerosEvidence, toUMAClaim, type Kleros1497Evidence, } from './envelope/serialize.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { canonicalStringify, digestOf, GENESIS_DIGEST, chainLinkDigest, CHAIN_CO
3
3
  export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, SIGNED_RECEIPT_FIELDS, } from './receipt/eip712.js';
4
4
  export { PUBLISHED_TEST_KEYS, publishedKeyLabel } from './receipt/known-keys.js';
5
5
  export { signerStatus } from './receipt/binding.js';
6
+ export { toCaip2Network } from './receipt/network.js';
6
7
  export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, COMPLIANCE_WIRE_VECTOR, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, } from './compliance/record.js';
7
8
  export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, } from './idempotency/middleware.js';
8
9
  export { D1IdempotencyStore, D1_IDEMPOTENCY_DDL } from './idempotency/d1.js';
@@ -10,7 +11,7 @@ export { DISPUTE_DOMAIN, EVIDENCE_DOMAIN, CRITERIA_DOMAIN, DISPUTE_TYPES, EVIDEN
10
11
  export { ACTION_DOMAIN, ACTION_TYPES, ACTION_WIRE_VECTOR, actionDigest, signActionRecord, verifyActionRecord, } from './evidence/action.js';
11
12
  export { recordDisclosure } from './evidence/disclose.js';
12
13
  export { LedgerClient } from './ledgerClient.js';
13
- export { Assure, attachToExtensions } from './assure.js';
14
- export { withAssure, extractSettlement, extractPaymentPayload } from './adapter/x402.js';
14
+ export { Assure, attachToExtensions, attachToSettlementResponse, OFFER_RECEIPT_RESPONSE_SCHEMA, } from './assure.js';
15
+ export { withAssure, extractSettlement, extractPaymentPayload, encodeX402Header, decodeX402Header, COMPLIANCE_FIELDS_ADVERTISEMENT_SCHEMA, } from './adapter/x402.js';
15
16
  export { ENVELOPE_STATEMENT_MAX_CHARS, VENUE_SUBMISSION_MAX_CHARS, ENVELOPE_VENUES, } from './envelope/types.js';
16
17
  export { assertEnvelopeCaps, toInternetCourtSubmission, toKlerosEvidence, toUMAClaim, } from './envelope/serialize.js';
@@ -22,8 +22,11 @@ export declare class LedgerClient {
22
22
  constructor(cfg: LedgerConfig);
23
23
  private get f();
24
24
  submitReceipt(artifact: SignedReceipt, compliance?: SignedComplianceRecord): Promise<CountersignResult>;
25
+ /** Log a refund against a receipt on this seller's chain. The ledger stores a PENDING refund
26
+ * entry; it is not counter-signed or chained, so the answer carries no digest or seq. */
25
27
  recordRefund(originalDigest: `0x${string}`, amount: string, reason: string): Promise<{
26
28
  id: string;
29
+ status: string;
27
30
  }>;
28
31
  /** Fetch the venue-neutral evidence envelope for a chained artifact (public endpoint — works
29
32
  * without an API key; the apiKey/sellerId in cfg are unused here). `statement` is an optional
@@ -23,6 +23,8 @@ export class LedgerClient {
23
23
  throw new Error(`ledger submit failed: ${res.status} ${await res.text()}`);
24
24
  return (await res.json());
25
25
  }
26
+ /** Log a refund against a receipt on this seller's chain. The ledger stores a PENDING refund
27
+ * entry; it is not counter-signed or chained, so the answer carries no digest or seq. */
26
28
  async recordRefund(originalDigest, amount, reason) {
27
29
  const res = await this.f(`${this.cfg.url}/v1/refunds`, {
28
30
  method: 'POST',
@@ -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.5.0";
11
+ readonly version: "0.6.1";
12
12
  };
13
13
  export declare function buildServer(deps: McpDeps): McpServer;
@@ -53,12 +53,12 @@ function json(value) {
53
53
  }
54
54
  /** MUST match package.json name/version — the MCP handshake self-reports this identity to
55
55
  * every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
56
- export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.5.0' };
56
+ export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.6.1' };
57
57
  export function buildServer(deps) {
58
58
  const server = new McpServer(MCP_SERVER_IDENTITY);
59
59
  server.registerTool('issue_receipt', {
60
60
  title: 'Issue signed receipt',
61
- description: 'Issue an x402 offer-receipt (EIP-712) plus a Tersign compliance record (returned as `compliance`; verify_compliance_record checks it) for a payment that has ALREADY settled, and counter-sign both into your hash chain when a ledger is configured. ' +
61
+ description: 'Issue an x402 offer-receipt (EIP-712) plus a Tersign compliance record (returned as `compliance`; verify_compliance_record checks it) for a payment that has ALREADY settled; when a ledger is configured, the ledger counter-signs the receipt into your hash chain, while the compliance-fields record is not counter-signed and is bound to its receipt only by your own signature, which covers the receipt\'s digest. ' +
62
62
  'Use this for money that moved; use record_disclosure for a non-payment agent action. ' +
63
63
  'Side effects: signs with your signing key (TERSIGN_SELLER_KEY when set, else the one generated and kept locally on first run), 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). ' +
64
64
  'Returns the signed receipt artifact, its keccak256 canonical digest, and — when chained — the ledger counter-signature and sequence number.',
@@ -133,10 +133,10 @@ export function buildServer(deps) {
133
133
  }, async ({ record, attestation, expectedSigner }) => json(await verifyRecordTool(record, attestation, expectedSigner)));
134
134
  server.registerTool('record_refund', {
135
135
  title: 'Record refund',
136
- 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. ' +
136
+ description: 'Log a refund against a receipt already on your chain, as the SELLER. The ledger stores it as a PENDING refund entry that references the original receipt digest; the entry is NOT counter-signed or appended to the hash chain, and it has no digest or sequence number of its own. Nothing is edited or deleted: the original receipt and its chain position stay exactly as they were. ' +
137
137
  '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. ' +
138
- 'This RECORDS a refund you have already made — it moves no money. ' +
139
- 'Returns the refund record, its digest, the ledger counter-signature and sequence number.',
138
+ 'This RECORDS a refund you have already made — it moves no money. For a signed refund RECORD, build a compliance record with refundOf set to the original record digest (buildMinimalRecord in the SDK). ' +
139
+ 'Returns { id, status: "pending" }.',
140
140
  inputSchema: {
141
141
  originalDigest: z
142
142
  .string()
@@ -190,9 +190,9 @@ export function buildServer(deps) {
190
190
  })));
191
191
  server.registerTool('adjudicate_dispute', {
192
192
  title: 'Adjudicate dispute',
193
- 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. ' +
194
- '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. ' +
195
- 'Returns the verdict, the rationale naming the rule applied, and the ledger signature over both.',
193
+ description: 'Trigger deterministic adjudication of an open dispute: the same inputs always produce the same verdict and rationale — no discretion, no model in the loop — and the verdict object embeds the inputs it was computed from. ' +
194
+ 'Side effects: writes the verdict to the dispute record; a refund verdict also logs a PENDING refund entry against the receipt (not counter-signed into the chain; no money moves). Adjudicating twice is not meaningful; the first verdict stands (a second call returns 409). ' +
195
+ 'Returns the verdict, the rationale naming the rule applied, the adjudication inputs, the verdict digest, and the ledger signature over that digest.',
196
196
  inputSchema: {
197
197
  disputeDigest: digestSchema.describe('0x-prefixed digest of the open dispute to adjudicate, as returned by open_dispute'),
198
198
  },
@@ -52,6 +52,7 @@ export declare function verifyReceiptTool(artifact: SignedReceipt, expectedSigne
52
52
  export declare function verifyRecordTool(record: ComplianceRecordV1, attestation: SignedComplianceRecord['attestation'], expectedSigner?: string): Promise<VerifyToolResult>;
53
53
  export declare function recordRefundTool(deps: McpDeps, originalDigest: `0x${string}`, amount: string, reason: string): Promise<{
54
54
  id: string;
55
+ status: string;
55
56
  }>;
56
57
  export interface OpenDisputeArgs {
57
58
  receiptDigest: `0x${string}`;
@@ -68,8 +69,8 @@ export interface SubmitEvidenceArgs {
68
69
  artifacts: EvidenceArtifactRef[];
69
70
  }
70
71
  export declare function submitEvidenceTool(deps: McpDeps, args: SubmitEvidenceArgs): Promise<unknown>;
71
- /** Trigger deterministic adjudication (public — the rulebook is recomputable, so anyone
72
- * may pull the trigger once the route guard allows it). */
72
+ /** Trigger deterministic adjudication (no API key — the outcome is a deterministic function of
73
+ * the recorded inputs, so anyone may pull the trigger once the route guard allows it). */
73
74
  export declare function adjudicateDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
74
75
  export declare function getDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
75
76
  export interface RecordDisclosureArgs {
package/dist/mcp/tools.js CHANGED
@@ -119,8 +119,8 @@ export async function submitEvidenceTool(deps, args) {
119
119
  ...(args.role === 'respondent' && deps.ledgerHttp.apiKey !== undefined ? { apiKey: deps.ledgerHttp.apiKey } : {}),
120
120
  });
121
121
  }
122
- /** Trigger deterministic adjudication (public — the rulebook is recomputable, so anyone
123
- * may pull the trigger once the route guard allows it). */
122
+ /** Trigger deterministic adjudication (no API key — the outcome is a deterministic function of
123
+ * the recorded inputs, so anyone may pull the trigger once the route guard allows it). */
124
124
  export async function adjudicateDisputeTool(deps, disputeDigest) {
125
125
  if (!deps.ledgerHttp)
126
126
  throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
@@ -32,7 +32,10 @@ export declare function signerStatus(r: SignerBinding, expected: string | undefi
32
32
  export declare function bindSigner(signer: `0x${string}`, expected: string | undefined, mismatchReason: string): SignerBinding;
33
33
  /** Why an EIP-712 `signature` is not THE canonical encoding, or undefined. Checked BEFORE
34
34
  * recovery, with the same rules as the Python twin's verify_receipt (sdk-py tersign/verify.py
35
- * signature_error). The canonical encoding is exactly one string per signer and message:
35
+ * signature_error). Each signature has exactly one accepted string, so a re-encoding of it is
36
+ * refused. That is a property of one signature, not of a signer and message: the signer can
37
+ * produce other valid signatures over the same message, so key any deduplication on (signer,
38
+ * message), never on the signature bytes. The accepted string is:
36
39
  *
37
40
  * "0x" + 130 LOWER-CASE hex digits = r (32 bytes) || s (32 bytes) || v (1 byte), with
38
41
  * 1 <= r, s < n; s <= n/2 (low-s); v = 27 or 28 (0x1b / 0x1c) — what viem's
@@ -48,7 +48,10 @@ export function bindSigner(signer, expected, mismatchReason) {
48
48
  const SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
49
49
  /** Why an EIP-712 `signature` is not THE canonical encoding, or undefined. Checked BEFORE
50
50
  * recovery, with the same rules as the Python twin's verify_receipt (sdk-py tersign/verify.py
51
- * signature_error). The canonical encoding is exactly one string per signer and message:
51
+ * signature_error). Each signature has exactly one accepted string, so a re-encoding of it is
52
+ * refused. That is a property of one signature, not of a signer and message: the signer can
53
+ * produce other valid signatures over the same message, so key any deduplication on (signer,
54
+ * message), never on the signature bytes. The accepted string is:
52
55
  *
53
56
  * "0x" + 130 LOWER-CASE hex digits = r (32 bytes) || s (32 bytes) || v (1 byte), with
54
57
  * 1 <= r, s < n; s <= n/2 (low-s); v = 27 or 28 (0x1b / 0x1c) — what viem's
@@ -0,0 +1,31 @@
1
+ /** x402 v1 network names → CAIP-2, for the receipt payload.
2
+ *
3
+ * The offer-receipt extension requires CAIP-2 in the signed payload whatever the protocol version:
4
+ * "Servers MUST convert v1 network identifiers (e.g., "base-sepolia") to CAIP-2 format (e.g.,
5
+ * "eip155:84532") in the receipt payload" (x402-foundation/x402 specs/extensions/
6
+ * extension-offer-and-receipt.md, receipt section; present at main 6b6ee91fee02, read 2026-09-29).
7
+ * A v1 settlement response carries the v1 name (`"base"`), so a receipt built from it without this
8
+ * step signs a payload that fails the spec.
9
+ *
10
+ * The EVM table is upstream `@x402/evm`'s `EVM_NETWORK_CHAIN_ID_MAP`, copied as written
11
+ * (typescript/packages/mechanisms/evm/src/constants.ts, blob ab7db42cc1c0: main 6b6ee91fee02, last
12
+ * changed a9955ae5538e, read 2026-09-29). Those 24 names are the ones upstream's v1 EVM facilitator
13
+ * settles and reports back as `network`, so each can reach a receipt. The offer-receipt reference's
14
+ * own table (typescript/packages/extensions/src/offer-receipt/signing.ts `V1_EVM_NETWORK_CHAIN_IDS`,
15
+ * same commit) holds 17 of them, with the same ids. The Solana table is signing.ts
16
+ * `V1_SOLANA_NETWORKS`, identical to `@x402/svm`'s `V1_TO_V2_NETWORK_MAP`
17
+ * (mechanisms/svm/src/constants.ts, same commit). The rules are signing.ts
18
+ * `convertNetworkStringToCAIP2`'s: a string containing `:` passes through unchanged, a known v1 name
19
+ * is looked up case-insensitively, anything else throws. One difference: the lookup reads own keys
20
+ * only, so an inherited object key such as `constructor` throws instead of resolving. Re-check
21
+ * against both upstream files when a v1 network is added there; test/network.test.ts pins both
22
+ * tables by exact equality.
23
+ *
24
+ * The tables are exported for that test only; the package entry re-exports `toCaip2Network`, not
25
+ * them. */
26
+ export declare const V1_EVM_NETWORK_CHAIN_IDS: Readonly<Record<string, number>>;
27
+ export declare const V1_SOLANA_NETWORKS: Readonly<Record<string, string>>;
28
+ /** A CAIP-2 identifier passes through; a known x402 v1 name (`"base"`, `"base-sepolia"`,
29
+ * `"solana"`, …) becomes its CAIP-2 form; anything else throws, because a receipt cannot carry
30
+ * it. */
31
+ export declare function toCaip2Network(network: string): string;
@@ -0,0 +1,71 @@
1
+ /** x402 v1 network names → CAIP-2, for the receipt payload.
2
+ *
3
+ * The offer-receipt extension requires CAIP-2 in the signed payload whatever the protocol version:
4
+ * "Servers MUST convert v1 network identifiers (e.g., "base-sepolia") to CAIP-2 format (e.g.,
5
+ * "eip155:84532") in the receipt payload" (x402-foundation/x402 specs/extensions/
6
+ * extension-offer-and-receipt.md, receipt section; present at main 6b6ee91fee02, read 2026-09-29).
7
+ * A v1 settlement response carries the v1 name (`"base"`), so a receipt built from it without this
8
+ * step signs a payload that fails the spec.
9
+ *
10
+ * The EVM table is upstream `@x402/evm`'s `EVM_NETWORK_CHAIN_ID_MAP`, copied as written
11
+ * (typescript/packages/mechanisms/evm/src/constants.ts, blob ab7db42cc1c0: main 6b6ee91fee02, last
12
+ * changed a9955ae5538e, read 2026-09-29). Those 24 names are the ones upstream's v1 EVM facilitator
13
+ * settles and reports back as `network`, so each can reach a receipt. The offer-receipt reference's
14
+ * own table (typescript/packages/extensions/src/offer-receipt/signing.ts `V1_EVM_NETWORK_CHAIN_IDS`,
15
+ * same commit) holds 17 of them, with the same ids. The Solana table is signing.ts
16
+ * `V1_SOLANA_NETWORKS`, identical to `@x402/svm`'s `V1_TO_V2_NETWORK_MAP`
17
+ * (mechanisms/svm/src/constants.ts, same commit). The rules are signing.ts
18
+ * `convertNetworkStringToCAIP2`'s: a string containing `:` passes through unchanged, a known v1 name
19
+ * is looked up case-insensitively, anything else throws. One difference: the lookup reads own keys
20
+ * only, so an inherited object key such as `constructor` throws instead of resolving. Re-check
21
+ * against both upstream files when a v1 network is added there; test/network.test.ts pins both
22
+ * tables by exact equality.
23
+ *
24
+ * The tables are exported for that test only; the package entry re-exports `toCaip2Network`, not
25
+ * them. */
26
+ export const V1_EVM_NETWORK_CHAIN_IDS = {
27
+ ethereum: 1,
28
+ sepolia: 11155111,
29
+ abstract: 2741,
30
+ 'abstract-testnet': 11124,
31
+ 'base-sepolia': 84532,
32
+ base: 8453,
33
+ 'avalanche-fuji': 43113,
34
+ avalanche: 43114,
35
+ iotex: 4689,
36
+ sei: 1329,
37
+ 'sei-testnet': 1328,
38
+ polygon: 137,
39
+ 'polygon-amoy': 80002,
40
+ peaq: 3338,
41
+ story: 1514,
42
+ educhain: 41923,
43
+ 'skale-base-sepolia': 324705682,
44
+ megaeth: 4326,
45
+ monad: 143,
46
+ 'monad-testnet': 10143,
47
+ stable: 988,
48
+ 'stable-testnet': 2201,
49
+ celo: 42220,
50
+ flare: 14,
51
+ };
52
+ export const V1_SOLANA_NETWORKS = {
53
+ solana: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
54
+ 'solana-devnet': 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
55
+ 'solana-testnet': 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z',
56
+ };
57
+ /** A CAIP-2 identifier passes through; a known x402 v1 name (`"base"`, `"base-sepolia"`,
58
+ * `"solana"`, …) becomes its CAIP-2 form; anything else throws, because a receipt cannot carry
59
+ * it. */
60
+ export function toCaip2Network(network) {
61
+ if (network.includes(':'))
62
+ return network;
63
+ const key = network.toLowerCase();
64
+ const chainId = Object.hasOwn(V1_EVM_NETWORK_CHAIN_IDS, key) ? V1_EVM_NETWORK_CHAIN_IDS[key] : undefined;
65
+ if (chainId !== undefined)
66
+ return `eip155:${chainId}`;
67
+ const solana = Object.hasOwn(V1_SOLANA_NETWORKS, key) ? V1_SOLANA_NETWORKS[key] : undefined;
68
+ if (solana !== undefined)
69
+ return solana;
70
+ throw new Error(`Unknown network identifier: "${network}". A receipt needs CAIP-2 (e.g. "eip155:8453") or an x402 v1 name (e.g. "base", "solana").`);
71
+ }