tersign 0.0.1 → 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -3
  3. package/dist/adapter/x402.d.ts +29 -0
  4. package/dist/adapter/x402.js +105 -0
  5. package/dist/assure.d.ts +47 -0
  6. package/dist/assure.js +68 -0
  7. package/dist/canonical.d.ts +4 -0
  8. package/dist/canonical.js +23 -0
  9. package/dist/compliance/record.d.ts +45 -0
  10. package/dist/compliance/record.js +94 -0
  11. package/dist/compliance/types.d.ts +6 -0
  12. package/dist/compliance/types.js +1 -0
  13. package/dist/dispute/sign.d.ts +80 -0
  14. package/dist/dispute/sign.js +212 -0
  15. package/dist/dispute/types.d.ts +98 -0
  16. package/dist/dispute/types.js +1 -0
  17. package/dist/envelope/serialize.d.ts +30 -0
  18. package/dist/envelope/serialize.js +73 -0
  19. package/dist/envelope/types.d.ts +49 -0
  20. package/dist/envelope/types.js +15 -0
  21. package/dist/evidence/action.d.ts +93 -0
  22. package/dist/evidence/action.js +70 -0
  23. package/dist/idempotency/d1.d.ts +32 -0
  24. package/dist/idempotency/d1.js +46 -0
  25. package/dist/idempotency/middleware.d.ts +63 -0
  26. package/dist/idempotency/middleware.js +0 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.js +13 -0
  29. package/dist/ledgerClient.d.ts +34 -0
  30. package/dist/ledgerClient.js +58 -0
  31. package/dist/mcp/bin.d.ts +2 -0
  32. package/dist/mcp/bin.js +5 -0
  33. package/dist/mcp/server.d.ts +7 -0
  34. package/dist/mcp/server.js +131 -0
  35. package/dist/mcp/tools.d.ts +57 -0
  36. package/dist/mcp/tools.js +85 -0
  37. package/dist/receipt/eip712.d.ts +72 -0
  38. package/dist/receipt/eip712.js +95 -0
  39. package/dist/types.d.ts +126 -0
  40. package/dist/types.js +5 -0
  41. package/dist/verify-bin.d.ts +2 -0
  42. package/dist/verify-bin.js +64 -0
  43. package/package.json +57 -4
  44. package/index.js +0 -1
@@ -0,0 +1,32 @@
1
+ import type { CachedResponse, IdempotencyStore } from './middleware.js';
2
+ /** Structural subset of Cloudflare's D1Database — declared locally so the SDK carries no
3
+ * @cloudflare/workers-types dependency; a real D1 binding satisfies it as-is. */
4
+ export interface D1Like {
5
+ prepare(query: string): {
6
+ bind(...values: unknown[]): {
7
+ first<T = unknown>(): Promise<T | null>;
8
+ run(): Promise<{
9
+ meta?: {
10
+ changes?: number;
11
+ };
12
+ }>;
13
+ };
14
+ };
15
+ }
16
+ /** DDL for the table this store expects. Run once per database (idempotent). */
17
+ export declare const D1_IDEMPOTENCY_DDL = "CREATE TABLE IF NOT EXISTS assure_idempotency (\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n response_json TEXT,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (scope, key)\n);";
18
+ /** Durable IdempotencyStore on Cloudflare D1 for sellers running on Workers — the
19
+ * MemoryIdempotencyStore resets on every isolate recycle, which silently re-executes
20
+ * paid work on retry. Reservation atomicity rides on the primary key: INSERT OR IGNORE
21
+ * reports 0 changes when another request holds the id, which maps to `in-flight`. */
22
+ export declare class D1IdempotencyStore implements IdempotencyStore {
23
+ private readonly db;
24
+ private readonly table;
25
+ constructor(db: D1Like, table?: string);
26
+ get(scope: string, id: string): Promise<{
27
+ fingerprint: string;
28
+ response: CachedResponse | null;
29
+ } | undefined>;
30
+ reserve(scope: string, id: string, fingerprint: string): Promise<boolean>;
31
+ complete(scope: string, id: string, response: CachedResponse): Promise<void>;
32
+ }
@@ -0,0 +1,46 @@
1
+ /** DDL for the table this store expects. Run once per database (idempotent). */
2
+ export const D1_IDEMPOTENCY_DDL = `CREATE TABLE IF NOT EXISTS assure_idempotency (
3
+ scope TEXT NOT NULL,
4
+ key TEXT NOT NULL,
5
+ fingerprint TEXT NOT NULL,
6
+ response_json TEXT,
7
+ created_at INTEGER NOT NULL,
8
+ PRIMARY KEY (scope, key)
9
+ );`;
10
+ /** Durable IdempotencyStore on Cloudflare D1 for sellers running on Workers — the
11
+ * MemoryIdempotencyStore resets on every isolate recycle, which silently re-executes
12
+ * paid work on retry. Reservation atomicity rides on the primary key: INSERT OR IGNORE
13
+ * reports 0 changes when another request holds the id, which maps to `in-flight`. */
14
+ export class D1IdempotencyStore {
15
+ db;
16
+ table;
17
+ constructor(db, table = 'assure_idempotency') {
18
+ this.db = db;
19
+ this.table = table;
20
+ }
21
+ async get(scope, id) {
22
+ const row = await this.db
23
+ .prepare(`SELECT fingerprint, response_json FROM ${this.table} WHERE scope = ? AND key = ?`)
24
+ .bind(scope, id)
25
+ .first();
26
+ if (!row)
27
+ return undefined;
28
+ return {
29
+ fingerprint: row.fingerprint,
30
+ response: row.response_json === null ? null : JSON.parse(row.response_json),
31
+ };
32
+ }
33
+ async reserve(scope, id, fingerprint) {
34
+ const result = await this.db
35
+ .prepare(`INSERT OR IGNORE INTO ${this.table} (scope, key, fingerprint, response_json, created_at) VALUES (?, ?, ?, NULL, ?)`)
36
+ .bind(scope, id, fingerprint, Math.floor(Date.now() / 1000))
37
+ .run();
38
+ return (result.meta?.changes ?? 0) === 1;
39
+ }
40
+ async complete(scope, id, response) {
41
+ await this.db
42
+ .prepare(`UPDATE ${this.table} SET response_json = ? WHERE scope = ? AND key = ?`)
43
+ .bind(JSON.stringify(response), scope, id)
44
+ .run();
45
+ }
46
+ }
@@ -0,0 +1,63 @@
1
+ /** Idempotency ENFORCEMENT for x402/MPP sellers. The x402 `payment-identifier` extension
2
+ * ships the key; retry/replay semantics are explicitly left to the application layer
3
+ * (x402 issue #452). This module implements the strictest published behavior table
4
+ * (ACP-grade): new id → process; same id + same fingerprint → cached replay; same id +
5
+ * different fingerprint → 409; required-but-missing → 400. */
6
+ export interface CachedResponse {
7
+ status: number;
8
+ headers: Record<string, string>;
9
+ body: string;
10
+ }
11
+ export interface IdempotencyStore {
12
+ get(scope: string, id: string): Promise<{
13
+ fingerprint: string;
14
+ response: CachedResponse | null;
15
+ } | undefined>;
16
+ /** reserve an id before processing (null response = in flight) */
17
+ reserve(scope: string, id: string, fingerprint: string): Promise<boolean>;
18
+ complete(scope: string, id: string, response: CachedResponse): Promise<void>;
19
+ }
20
+ export declare class MemoryIdempotencyStore implements IdempotencyStore {
21
+ private m;
22
+ get(scope: string, id: string): Promise<{
23
+ fingerprint: string;
24
+ response: CachedResponse | null;
25
+ } | undefined>;
26
+ reserve(scope: string, id: string, fingerprint: string): Promise<boolean>;
27
+ complete(scope: string, id: string, response: CachedResponse): Promise<void>;
28
+ }
29
+ /** Extract the payment-identifier id from an x402 PaymentPayload's extensions, if present. */
30
+ export declare function extractPaymentId(paymentPayload: unknown): string | undefined;
31
+ export interface FingerprintParts {
32
+ method: string;
33
+ path: string;
34
+ scheme?: string;
35
+ network?: string;
36
+ asset?: string;
37
+ amount?: string;
38
+ payTo?: string;
39
+ operation?: string;
40
+ }
41
+ /** Per the payment-identifier spec: bind the id to a normalized request fingerprint. */
42
+ export declare function fingerprint(parts: FingerprintParts): string;
43
+ export type IdempotencyOutcome = {
44
+ kind: 'process';
45
+ onComplete: (response: CachedResponse) => Promise<void>;
46
+ } | {
47
+ kind: 'replay';
48
+ response: CachedResponse;
49
+ } | {
50
+ kind: 'in-flight';
51
+ } | {
52
+ kind: 'conflict';
53
+ } | {
54
+ kind: 'missing';
55
+ };
56
+ export interface IdempotencyOptions {
57
+ store: IdempotencyStore;
58
+ required: boolean;
59
+ /** tenant/route scope so ids never collide across sellers (spec guidance) */
60
+ scope: string;
61
+ }
62
+ export declare function checkIdempotency(opts: IdempotencyOptions, id: string | undefined, fp: string): Promise<IdempotencyOutcome>;
63
+ export declare const REPLAY_HEADER = "Idempotent-Replayed";
Binary file
@@ -0,0 +1,14 @@
1
+ export * from './types.js';
2
+ export { canonicalStringify, digestOf } from './canonical.js';
3
+ export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, type VerifyResult, } from './receipt/eip712.js';
4
+ export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, type IssuerConfig, type MinimalRecordInput, } from './compliance/record.js';
5
+ export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, type IdempotencyStore, type IdempotencyOutcome, type IdempotencyOptions, type CachedResponse, type FingerprintParts, } from './idempotency/middleware.js';
6
+ export { D1IdempotencyStore, D1_IDEMPOTENCY_DDL, type D1Like } from './idempotency/d1.js';
7
+ export type { DisputeReason, DisputeVerdict, DisputeStatus, DisputePayloadV1, CriterionV1, AcceptanceCriteriaV1, EvidenceArtifactRef, EvidencePayloadV1, DisputeAttestationPayload, EvidenceAttestationPayload, CriteriaAttestationPayload, SignedDispute, SignedEvidence, SignedCriteria, } from './dispute/types.js';
8
+ export { DISPUTE_DOMAIN, EVIDENCE_DOMAIN, CRITERIA_DOMAIN, DISPUTE_TYPES, EVIDENCE_TYPES, CRITERIA_TYPES, DISPUTE_WIRE_VECTOR, disputeDigest, evidenceDigest, criteriaDigest, signDispute, verifyDispute, signEvidence, verifyEvidence, signCriteria, verifyCriteria, } from './dispute/sign.js';
9
+ 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';
10
+ export { LedgerClient, type LedgerConfig, type CountersignResult } from './ledgerClient.js';
11
+ export { Assure, attachToExtensions, type AssureConfig, type SettlementContext, type IssuedReceipt } from './assure.js';
12
+ export { withAssure, extractSettlement, extractPaymentPayload, type WithAssureConfig, type SettlementInfo } from './adapter/x402.js';
13
+ export { ENVELOPE_STATEMENT_MAX_CHARS, VENUE_SUBMISSION_MAX_CHARS, ENVELOPE_VENUES, type EvidenceEnvelopeV1, type EnvelopeSubjectKind, type EnvelopeVenue, } from './envelope/types.js';
14
+ export { assertEnvelopeCaps, toInternetCourtSubmission, toKlerosEvidence, toUMAClaim, type Kleros1497Evidence, } from './envelope/serialize.js';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from './types.js';
2
+ export { canonicalStringify, digestOf } from './canonical.js';
3
+ export { RECEIPT_DOMAIN, RECEIPT_TYPES, OFFER_DOMAIN, OFFER_TYPES, signReceipt, signOffer, verifyReceipt, } from './receipt/eip712.js';
4
+ export { COMPLIANCE_DOMAIN, COMPLIANCE_TYPES, buildMinimalRecord, recordDigest, signComplianceRecord, verifyComplianceRecord, } from './compliance/record.js';
5
+ export { MemoryIdempotencyStore, checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, } from './idempotency/middleware.js';
6
+ export { D1IdempotencyStore, D1_IDEMPOTENCY_DDL } from './idempotency/d1.js';
7
+ export { DISPUTE_DOMAIN, EVIDENCE_DOMAIN, CRITERIA_DOMAIN, DISPUTE_TYPES, EVIDENCE_TYPES, CRITERIA_TYPES, DISPUTE_WIRE_VECTOR, disputeDigest, evidenceDigest, criteriaDigest, signDispute, verifyDispute, signEvidence, verifyEvidence, signCriteria, verifyCriteria, } from './dispute/sign.js';
8
+ export { ACTION_DOMAIN, ACTION_TYPES, ACTION_WIRE_VECTOR, actionDigest, signActionRecord, verifyActionRecord, } from './evidence/action.js';
9
+ export { LedgerClient } from './ledgerClient.js';
10
+ export { Assure, attachToExtensions } from './assure.js';
11
+ export { withAssure, extractSettlement, extractPaymentPayload } from './adapter/x402.js';
12
+ export { ENVELOPE_STATEMENT_MAX_CHARS, VENUE_SUBMISSION_MAX_CHARS, ENVELOPE_VENUES, } from './envelope/types.js';
13
+ export { assertEnvelopeCaps, toInternetCourtSubmission, toKlerosEvidence, toUMAClaim, } from './envelope/serialize.js';
@@ -0,0 +1,34 @@
1
+ import type { SignedComplianceRecord, SignedReceipt } from './types.js';
2
+ import type { EnvelopeVenue, EvidenceEnvelopeV1 } from './envelope/types.js';
3
+ import { type Kleros1497Evidence } from './envelope/serialize.js';
4
+ export interface LedgerConfig {
5
+ url: string;
6
+ apiKey: string;
7
+ sellerId: string;
8
+ fetchImpl?: typeof fetch;
9
+ }
10
+ export interface CountersignResult {
11
+ id: string;
12
+ digest: `0x${string}`;
13
+ seq: number;
14
+ prevDigest: `0x${string}` | null;
15
+ countersignature: string;
16
+ }
17
+ /** Client for the hosted Tersign ledger: counter-signature + hash-chain + exports.
18
+ * The counter-signed chain is what makes a receipt independently verifiable and
19
+ * audit-exportable after the fact — the hosted half of the product. */
20
+ export declare class LedgerClient {
21
+ private cfg;
22
+ constructor(cfg: LedgerConfig);
23
+ private get f();
24
+ submitReceipt(artifact: SignedReceipt, compliance?: SignedComplianceRecord): Promise<CountersignResult>;
25
+ recordRefund(originalDigest: `0x${string}`, amount: string, reason: string): Promise<{
26
+ id: string;
27
+ }>;
28
+ /** Fetch the venue-neutral evidence envelope for a chained artifact (public endpoint — works
29
+ * without an API key; the apiKey/sellerId in cfg are unused here). `statement` is an optional
30
+ * party claim (≤500 chars) folded into the envelope, never raw evidence. */
31
+ fetchEnvelope(digest: `0x${string}`, statement?: string): Promise<EvidenceEnvelopeV1>;
32
+ /** Fetch + serialize in one call: a jury-ready submission for the named venue. */
33
+ fetchVenueSubmission(digest: `0x${string}`, venue: Exclude<EnvelopeVenue, 'generic'>, statement?: string): Promise<string | Kleros1497Evidence>;
34
+ }
@@ -0,0 +1,58 @@
1
+ import { toInternetCourtSubmission, toKlerosEvidence, toUMAClaim } from './envelope/serialize.js';
2
+ /** Client for the hosted Tersign ledger: counter-signature + hash-chain + exports.
3
+ * The counter-signed chain is what makes a receipt independently verifiable and
4
+ * audit-exportable after the fact — the hosted half of the product. */
5
+ export class LedgerClient {
6
+ cfg;
7
+ constructor(cfg) {
8
+ this.cfg = cfg;
9
+ }
10
+ get f() {
11
+ return this.cfg.fetchImpl ?? fetch;
12
+ }
13
+ async submitReceipt(artifact, compliance) {
14
+ const res = await this.f(`${this.cfg.url}/v1/receipts`, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'content-type': 'application/json',
18
+ authorization: `Bearer ${this.cfg.apiKey}`,
19
+ },
20
+ body: JSON.stringify({ sellerId: this.cfg.sellerId, artifact, compliance }),
21
+ });
22
+ if (!res.ok)
23
+ throw new Error(`ledger submit failed: ${res.status} ${await res.text()}`);
24
+ return (await res.json());
25
+ }
26
+ async recordRefund(originalDigest, amount, reason) {
27
+ const res = await this.f(`${this.cfg.url}/v1/refunds`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'content-type': 'application/json',
31
+ authorization: `Bearer ${this.cfg.apiKey}`,
32
+ },
33
+ body: JSON.stringify({ originalDigest, amount, reason }),
34
+ });
35
+ if (!res.ok)
36
+ throw new Error(`ledger refund record failed: ${res.status} ${await res.text()}`);
37
+ return (await res.json());
38
+ }
39
+ /** Fetch the venue-neutral evidence envelope for a chained artifact (public endpoint — works
40
+ * without an API key; the apiKey/sellerId in cfg are unused here). `statement` is an optional
41
+ * party claim (≤500 chars) folded into the envelope, never raw evidence. */
42
+ async fetchEnvelope(digest, statement) {
43
+ const q = statement !== undefined ? `?statement=${encodeURIComponent(statement)}` : '';
44
+ const res = await this.f(`${this.cfg.url}/v1/receipts/${digest}/envelope${q}`);
45
+ if (!res.ok)
46
+ throw new Error(`envelope fetch failed: ${res.status} ${await res.text()}`);
47
+ return (await res.json());
48
+ }
49
+ /** Fetch + serialize in one call: a jury-ready submission for the named venue. */
50
+ async fetchVenueSubmission(digest, venue, statement) {
51
+ const env = await this.fetchEnvelope(digest, statement);
52
+ if (venue === 'internet-court')
53
+ return toInternetCourtSubmission(env);
54
+ if (venue === 'kleros')
55
+ return toKlerosEvidence(env);
56
+ return toUMAClaim(env);
57
+ }
58
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { buildServer, envDeps } from './server.js';
4
+ const server = buildServer(envDeps());
5
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,7 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type McpDeps } from './tools.js';
3
+ /** MCP packaging: exposes assure as tools any MCP-speaking agent can call, so an agent
4
+ * (or its framework) can issue, verify, and chain receipts without importing the SDK.
5
+ * Config via env — see envDeps(). */
6
+ export declare function envDeps(env?: Record<string, string | undefined>): McpDeps;
7
+ export declare function buildServer(deps: McpDeps): McpServer;
@@ -0,0 +1,131 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { Assure } from '../assure.js';
5
+ import { LedgerClient } from '../ledgerClient.js';
6
+ import { adjudicateDisputeTool, getDisputeTool, issueReceiptTool, openDisputeTool, recordRefundTool, submitEvidenceTool, verifyReceiptTool, verifyRecordTool, } from './tools.js';
7
+ /** MCP packaging: exposes assure as tools any MCP-speaking agent can call, so an agent
8
+ * (or its framework) can issue, verify, and chain receipts without importing the SDK.
9
+ * Config via env — see envDeps(). */
10
+ export function envDeps(env = process.env) {
11
+ const key = env.TERSIGN_SELLER_KEY;
12
+ if (!key)
13
+ throw new Error('TERSIGN_SELLER_KEY (0x-prefixed private key) is required');
14
+ const account = privateKeyToAccount(key);
15
+ const assure = new Assure({
16
+ signer: account,
17
+ issuer: {
18
+ name: env.TERSIGN_ISSUER_NAME ?? 'unnamed seller',
19
+ jurisdiction: env.TERSIGN_ISSUER_JURISDICTION ?? 'unknown',
20
+ ...(env.TERSIGN_ISSUER_TAX_ID !== undefined ? { taxId: env.TERSIGN_ISSUER_TAX_ID } : {}),
21
+ },
22
+ ...(env.TERSIGN_LEDGER_URL && env.TERSIGN_LEDGER_API_KEY && env.TERSIGN_LEDGER_SELLER_ID
23
+ ? { ledger: { url: env.TERSIGN_LEDGER_URL, apiKey: env.TERSIGN_LEDGER_API_KEY, sellerId: env.TERSIGN_LEDGER_SELLER_ID } }
24
+ : {}),
25
+ });
26
+ const ledger = env.TERSIGN_LEDGER_URL && env.TERSIGN_LEDGER_API_KEY && env.TERSIGN_LEDGER_SELLER_ID
27
+ ? new LedgerClient({ url: env.TERSIGN_LEDGER_URL, apiKey: env.TERSIGN_LEDGER_API_KEY, sellerId: env.TERSIGN_LEDGER_SELLER_ID })
28
+ : undefined;
29
+ return {
30
+ assure,
31
+ signer: account,
32
+ ...(ledger ? { ledger } : {}),
33
+ ...(env.TERSIGN_LEDGER_URL
34
+ ? {
35
+ ledgerHttp: {
36
+ url: env.TERSIGN_LEDGER_URL,
37
+ ...(env.TERSIGN_LEDGER_API_KEY !== undefined ? { apiKey: env.TERSIGN_LEDGER_API_KEY } : {}),
38
+ },
39
+ }
40
+ : {}),
41
+ };
42
+ }
43
+ function json(value) {
44
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
45
+ }
46
+ export function buildServer(deps) {
47
+ const server = new McpServer({ name: 'tersign-assure', version: '0.0.1' });
48
+ server.registerTool('issue_receipt', {
49
+ title: 'Issue signed receipt',
50
+ description: 'Issue an x402 offer-receipt (EIP-712) plus an Tersign compliance record for a settled payment; counter-signs into the ledger when configured.',
51
+ inputSchema: {
52
+ network: z.string().describe('CAIP-2, e.g. eip155:8453'),
53
+ resourceUrl: z.string().url(),
54
+ payer: z.string(),
55
+ supplyDescription: z.string(),
56
+ settledAt: z.number().int().optional(),
57
+ txHash: z.string().optional(),
58
+ taxScheme: z.enum(['none', 'vat', 'gst', 'jct', 'sales']).optional(),
59
+ currency: z.string().optional(),
60
+ principal: z.string().optional().describe('signed principal behind the paying agent'),
61
+ },
62
+ }, async (args) => json(await issueReceiptTool(deps, args)));
63
+ server.registerTool('verify_receipt', {
64
+ title: 'Verify signed receipt',
65
+ description: 'Verify an offer-receipt artifact (EIP-712) and optionally enforce an expected signer (payTo authorization).',
66
+ inputSchema: {
67
+ artifact: z.record(z.unknown()).describe('the receipt artifact object {format, payload, signature}'),
68
+ expectedSigner: z.string().optional(),
69
+ },
70
+ }, async ({ artifact, expectedSigner }) => json(await verifyReceiptTool(artifact, expectedSigner)));
71
+ server.registerTool('verify_compliance_record', {
72
+ title: 'Verify compliance record',
73
+ description: 'Verify an Tersign compliance record + attestation (digest binding and signature).',
74
+ inputSchema: {
75
+ record: z.record(z.unknown()),
76
+ attestation: z.record(z.unknown()),
77
+ expectedSigner: z.string().optional(),
78
+ },
79
+ }, async ({ record, attestation, expectedSigner }) => json(await verifyRecordTool(record, attestation, expectedSigner)));
80
+ server.registerTool('record_refund', {
81
+ title: 'Record refund',
82
+ description: 'Record a refund against a receipt digest in the Tersign ledger (requires ledger configuration).',
83
+ inputSchema: {
84
+ originalDigest: z.string().regex(/^0x[0-9a-fA-F]{64}$/),
85
+ amount: z.string(),
86
+ reason: z.string(),
87
+ },
88
+ }, async ({ originalDigest, amount, reason }) => json(await recordRefundTool(deps, originalDigest, amount, reason)));
89
+ const digestSchema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
90
+ server.registerTool('open_dispute', {
91
+ title: 'Open dispute',
92
+ 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.',
93
+ inputSchema: {
94
+ receiptDigest: digestSchema,
95
+ reason: z.enum(['not_delivered', 'wrong_content', 'duplicate_charge']),
96
+ claimAmount: z.string().describe('claimed refund in the settlement currency'),
97
+ statement: z.string().optional().describe('for humans reading the record — never an adjudication input'),
98
+ },
99
+ }, async ({ receiptDigest, reason, claimAmount, statement }) => json(await openDisputeTool(deps, { receiptDigest: receiptDigest, reason, claimAmount, statement })));
100
+ server.registerTool('submit_dispute_evidence', {
101
+ title: 'Submit dispute evidence',
102
+ 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).',
103
+ inputSchema: {
104
+ disputeDigest: digestSchema,
105
+ role: z.enum(['claimant', 'respondent']),
106
+ artifacts: z
107
+ .array(z.object({
108
+ kind: z.enum(['content-digest', 'delivery-attestation', 'payment-proof', 'transcript']),
109
+ digest: digestSchema,
110
+ at: z.number().int().optional(),
111
+ note: z.string().optional(),
112
+ }))
113
+ .min(1),
114
+ },
115
+ }, async ({ disputeDigest, role, artifacts }) => json(await submitEvidenceTool(deps, {
116
+ disputeDigest: disputeDigest,
117
+ role,
118
+ artifacts: artifacts,
119
+ })));
120
+ server.registerTool('adjudicate_dispute', {
121
+ title: 'Adjudicate dispute',
122
+ description: 'Trigger deterministic adjudication of an open dispute (public — the v0 rulebook is recomputable by anyone). Refund verdicts create refund records automatically.',
123
+ inputSchema: { disputeDigest: digestSchema },
124
+ }, async ({ disputeDigest }) => json(await adjudicateDisputeTool(deps, disputeDigest)));
125
+ server.registerTool('get_dispute', {
126
+ title: 'Get dispute record',
127
+ description: 'Fetch a dispute record with its evidence, verdict, rationale, and ledger signature.',
128
+ inputSchema: { disputeDigest: digestSchema },
129
+ }, async ({ disputeDigest }) => json(await getDisputeTool(deps, disputeDigest)));
130
+ return server;
131
+ }
@@ -0,0 +1,57 @@
1
+ import type { Account } from 'viem/accounts';
2
+ import type { Assure } from '../assure.js';
3
+ import type { DisputeReason, EvidenceArtifactRef } from '../dispute/types.js';
4
+ import type { LedgerClient } from '../ledgerClient.js';
5
+ import type { ComplianceRecordV1, SignedComplianceRecord, SignedReceipt } from '../types.js';
6
+ /** Plain-function tool implementations, kept separate from MCP wiring so they are unit-testable
7
+ * and reusable from non-MCP surfaces. */
8
+ export interface McpDeps {
9
+ assure: Assure;
10
+ ledger?: LedgerClient;
11
+ /** signing key for dispute-side actions. Standing is key-based: opening a dispute
12
+ * requires this key to be the disputed receipt's payer. */
13
+ signer?: Account;
14
+ /** raw ledger HTTP access for the PUBLIC dispute endpoints (no API key needed;
15
+ * apiKey only authenticates respondent evidence). */
16
+ ledgerHttp?: {
17
+ url: string;
18
+ apiKey?: string;
19
+ };
20
+ clock?: () => number;
21
+ }
22
+ export interface IssueReceiptArgs {
23
+ network: string;
24
+ resourceUrl: string;
25
+ payer: string;
26
+ supplyDescription: string;
27
+ settledAt?: number | undefined;
28
+ txHash?: string | undefined;
29
+ taxScheme?: 'none' | 'vat' | 'gst' | 'jct' | 'sales' | undefined;
30
+ currency?: string | undefined;
31
+ principal?: string | undefined;
32
+ }
33
+ export declare function issueReceiptTool(deps: McpDeps, args: IssueReceiptArgs): Promise<import("../assure.js").IssuedReceipt>;
34
+ export declare function verifyReceiptTool(artifact: SignedReceipt, expectedSigner?: string): Promise<import("../index.js").VerifyResult>;
35
+ export declare function verifyRecordTool(record: ComplianceRecordV1, attestation: SignedComplianceRecord['attestation'], expectedSigner?: string): Promise<import("../compliance/types.js").VerifyLike>;
36
+ export declare function recordRefundTool(deps: McpDeps, originalDigest: `0x${string}`, amount: string, reason: string): Promise<{
37
+ id: string;
38
+ }>;
39
+ export interface OpenDisputeArgs {
40
+ receiptDigest: `0x${string}`;
41
+ reason: DisputeReason;
42
+ claimAmount: string;
43
+ statement?: string | undefined;
44
+ }
45
+ /** Open a dispute as the PAYER. The configured key signs the dispute; the ledger rejects
46
+ * it (403) unless the signature recovers to the disputed receipt's payer. */
47
+ export declare function openDisputeTool(deps: McpDeps, args: OpenDisputeArgs): Promise<unknown>;
48
+ export interface SubmitEvidenceArgs {
49
+ disputeDigest: `0x${string}`;
50
+ role: 'claimant' | 'respondent';
51
+ artifacts: EvidenceArtifactRef[];
52
+ }
53
+ export declare function submitEvidenceTool(deps: McpDeps, args: SubmitEvidenceArgs): Promise<unknown>;
54
+ /** Trigger deterministic adjudication (public — the rulebook is recomputable, so anyone
55
+ * may pull the trigger once the route guard allows it). */
56
+ export declare function adjudicateDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
57
+ export declare function getDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
@@ -0,0 +1,85 @@
1
+ import { verifyReceipt } from '../receipt/eip712.js';
2
+ import { verifyComplianceRecord } from '../compliance/record.js';
3
+ import { signDispute, signEvidence } from '../dispute/sign.js';
4
+ async function ledgerFetch(base, path, init) {
5
+ const url = `${base.replace(/\/$/, '')}${path}`;
6
+ const res = await fetch(url, {
7
+ method: init?.body === undefined ? 'GET' : 'POST',
8
+ headers: {
9
+ ...(init?.body !== undefined ? { 'content-type': 'application/json' } : {}),
10
+ ...(init?.apiKey ? { authorization: `Bearer ${init.apiKey}` } : {}),
11
+ },
12
+ ...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
13
+ });
14
+ const json = await res.json().catch(() => ({}));
15
+ if (!res.ok)
16
+ throw new Error(`ledger ${res.status}: ${JSON.stringify(json)}`);
17
+ return json;
18
+ }
19
+ export async function issueReceiptTool(deps, args) {
20
+ const now = deps.clock ?? (() => Math.floor(Date.now() / 1000));
21
+ const ctx = {
22
+ network: args.network,
23
+ resourceUrl: args.resourceUrl,
24
+ payer: args.payer,
25
+ settledAt: args.settledAt ?? now(),
26
+ supplyDescription: args.supplyDescription,
27
+ tax: { scheme: args.taxScheme ?? 'none', currency: args.currency ?? 'USD' },
28
+ ...(args.txHash !== undefined ? { txHash: args.txHash } : {}),
29
+ ...(args.principal !== undefined ? { buyer: { principal: args.principal } } : {}),
30
+ };
31
+ return deps.assure.issueFor(ctx);
32
+ }
33
+ export async function verifyReceiptTool(artifact, expectedSigner) {
34
+ return verifyReceipt(artifact, expectedSigner);
35
+ }
36
+ export async function verifyRecordTool(record, attestation, expectedSigner) {
37
+ return verifyComplianceRecord({ record, attestation }, expectedSigner);
38
+ }
39
+ export async function recordRefundTool(deps, originalDigest, amount, reason) {
40
+ if (!deps.ledger)
41
+ throw new Error('ledger not configured — set TERSIGN_LEDGER_URL / _API_KEY / _SELLER_ID');
42
+ return deps.ledger.recordRefund(originalDigest, amount, reason);
43
+ }
44
+ /** Open a dispute as the PAYER. The configured key signs the dispute; the ledger rejects
45
+ * it (403) unless the signature recovers to the disputed receipt's payer. */
46
+ export async function openDisputeTool(deps, args) {
47
+ if (!deps.signer)
48
+ throw new Error('no signing key configured — set TERSIGN_SELLER_KEY (used as the acting key)');
49
+ if (!deps.ledgerHttp)
50
+ throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
51
+ const now = deps.clock ?? (() => Math.floor(Date.now() / 1000));
52
+ const artifact = await signDispute({
53
+ version: 1,
54
+ receiptDigest: args.receiptDigest,
55
+ reason: args.reason,
56
+ claimAmount: args.claimAmount,
57
+ ...(args.statement !== undefined ? { statement: args.statement } : {}),
58
+ openedAt: now(),
59
+ }, deps.signer);
60
+ return ledgerFetch(deps.ledgerHttp.url, '/v1/disputes', { body: { artifact } });
61
+ }
62
+ export async function submitEvidenceTool(deps, args) {
63
+ if (!deps.signer)
64
+ throw new Error('no signing key configured');
65
+ if (!deps.ledgerHttp)
66
+ throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
67
+ const now = deps.clock ?? (() => Math.floor(Date.now() / 1000));
68
+ const artifact = await signEvidence({ version: 1, disputeDigest: args.disputeDigest, role: args.role, artifacts: args.artifacts, submittedAt: now() }, deps.signer);
69
+ return ledgerFetch(deps.ledgerHttp.url, `/v1/disputes/${args.disputeDigest}/evidence`, {
70
+ body: { artifact },
71
+ ...(args.role === 'respondent' && deps.ledgerHttp.apiKey !== undefined ? { apiKey: deps.ledgerHttp.apiKey } : {}),
72
+ });
73
+ }
74
+ /** Trigger deterministic adjudication (public — the rulebook is recomputable, so anyone
75
+ * may pull the trigger once the route guard allows it). */
76
+ export async function adjudicateDisputeTool(deps, disputeDigest) {
77
+ if (!deps.ledgerHttp)
78
+ throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
79
+ return ledgerFetch(deps.ledgerHttp.url, `/v1/disputes/${disputeDigest}/adjudicate`, { body: {} });
80
+ }
81
+ export async function getDisputeTool(deps, disputeDigest) {
82
+ if (!deps.ledgerHttp)
83
+ throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
84
+ return ledgerFetch(deps.ledgerHttp.url, `/v1/disputes/${disputeDigest}`);
85
+ }
@@ -0,0 +1,72 @@
1
+ import type { Account } from 'viem/accounts';
2
+ import type { OfferPayload, ReceiptPayload, SignedOffer, SignedReceipt } from '../types.js';
3
+ /** Canonical EIP-712 material from the merged offer-receipt extension. Domain chainId is
4
+ * hardcoded to 1 by spec (off-chain signing format; payment network lives in payload.network). */
5
+ export declare const RECEIPT_DOMAIN: {
6
+ readonly name: "x402 receipt";
7
+ readonly version: "1";
8
+ readonly chainId: 1n;
9
+ };
10
+ export declare const OFFER_DOMAIN: {
11
+ readonly name: "x402 offer";
12
+ readonly version: "1";
13
+ readonly chainId: 1n;
14
+ };
15
+ export declare const RECEIPT_TYPES: {
16
+ readonly Receipt: readonly [{
17
+ readonly name: "version";
18
+ readonly type: "uint256";
19
+ }, {
20
+ readonly name: "network";
21
+ readonly type: "string";
22
+ }, {
23
+ readonly name: "resourceUrl";
24
+ readonly type: "string";
25
+ }, {
26
+ readonly name: "payer";
27
+ readonly type: "string";
28
+ }, {
29
+ readonly name: "issuedAt";
30
+ readonly type: "uint256";
31
+ }, {
32
+ readonly name: "transaction";
33
+ readonly type: "string";
34
+ }];
35
+ };
36
+ export declare const OFFER_TYPES: {
37
+ readonly Offer: readonly [{
38
+ readonly name: "version";
39
+ readonly type: "uint256";
40
+ }, {
41
+ readonly name: "resourceUrl";
42
+ readonly type: "string";
43
+ }, {
44
+ readonly name: "scheme";
45
+ readonly type: "string";
46
+ }, {
47
+ readonly name: "network";
48
+ readonly type: "string";
49
+ }, {
50
+ readonly name: "asset";
51
+ readonly type: "string";
52
+ }, {
53
+ readonly name: "payTo";
54
+ readonly type: "string";
55
+ }, {
56
+ readonly name: "amount";
57
+ readonly type: "string";
58
+ }, {
59
+ readonly name: "validUntil";
60
+ readonly type: "uint256";
61
+ }];
62
+ };
63
+ export declare function signReceipt(payload: ReceiptPayload, account: Account): Promise<SignedReceipt>;
64
+ export declare function signOffer(payload: OfferPayload, account: Account, acceptIndex?: number): Promise<SignedOffer>;
65
+ export interface VerifyResult {
66
+ valid: boolean;
67
+ signer?: `0x${string}`;
68
+ reason?: string;
69
+ }
70
+ /** Verify an EIP-712 receipt. `expectedSigner` implements the spec's payTo-key authorization
71
+ * model; pass the seller's payTo address (or a registry-resolved key) to enforce it. */
72
+ export declare function verifyReceipt(artifact: SignedReceipt, expectedSigner?: string): Promise<VerifyResult>;