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,95 @@
1
+ import { recoverTypedDataAddress } from 'viem';
2
+ /** Canonical EIP-712 material from the merged offer-receipt extension. Domain chainId is
3
+ * hardcoded to 1 by spec (off-chain signing format; payment network lives in payload.network). */
4
+ export const RECEIPT_DOMAIN = { name: 'x402 receipt', version: '1', chainId: 1n };
5
+ export const OFFER_DOMAIN = { name: 'x402 offer', version: '1', chainId: 1n };
6
+ export const RECEIPT_TYPES = {
7
+ Receipt: [
8
+ { name: 'version', type: 'uint256' },
9
+ { name: 'network', type: 'string' },
10
+ { name: 'resourceUrl', type: 'string' },
11
+ { name: 'payer', type: 'string' },
12
+ { name: 'issuedAt', type: 'uint256' },
13
+ { name: 'transaction', type: 'string' },
14
+ ],
15
+ };
16
+ export const OFFER_TYPES = {
17
+ Offer: [
18
+ { name: 'version', type: 'uint256' },
19
+ { name: 'resourceUrl', type: 'string' },
20
+ { name: 'scheme', type: 'string' },
21
+ { name: 'network', type: 'string' },
22
+ { name: 'asset', type: 'string' },
23
+ { name: 'payTo', type: 'string' },
24
+ { name: 'amount', type: 'string' },
25
+ { name: 'validUntil', type: 'uint256' },
26
+ ],
27
+ };
28
+ function receiptMessage(p) {
29
+ return {
30
+ version: BigInt(p.version),
31
+ network: p.network,
32
+ resourceUrl: p.resourceUrl,
33
+ payer: p.payer,
34
+ issuedAt: BigInt(p.issuedAt),
35
+ transaction: p.transaction,
36
+ };
37
+ }
38
+ function offerMessage(p) {
39
+ return {
40
+ version: BigInt(p.version),
41
+ resourceUrl: p.resourceUrl,
42
+ scheme: p.scheme,
43
+ network: p.network,
44
+ asset: p.asset,
45
+ payTo: p.payTo,
46
+ amount: p.amount,
47
+ validUntil: BigInt(p.validUntil),
48
+ };
49
+ }
50
+ export async function signReceipt(payload, account) {
51
+ if (!account.signTypedData)
52
+ throw new Error('account cannot sign typed data');
53
+ const signature = await account.signTypedData({
54
+ domain: RECEIPT_DOMAIN,
55
+ types: RECEIPT_TYPES,
56
+ primaryType: 'Receipt',
57
+ message: receiptMessage(payload),
58
+ });
59
+ return { format: 'eip712', payload, signature };
60
+ }
61
+ export async function signOffer(payload, account, acceptIndex) {
62
+ if (!account.signTypedData)
63
+ throw new Error('account cannot sign typed data');
64
+ const signature = await account.signTypedData({
65
+ domain: OFFER_DOMAIN,
66
+ types: OFFER_TYPES,
67
+ primaryType: 'Offer',
68
+ message: offerMessage(payload),
69
+ });
70
+ return acceptIndex === undefined
71
+ ? { format: 'eip712', payload, signature }
72
+ : { format: 'eip712', payload, signature, acceptIndex };
73
+ }
74
+ /** Verify an EIP-712 receipt. `expectedSigner` implements the spec's payTo-key authorization
75
+ * model; pass the seller's payTo address (or a registry-resolved key) to enforce it. */
76
+ export async function verifyReceipt(artifact, expectedSigner) {
77
+ if (artifact.format !== 'eip712')
78
+ return { valid: false, reason: 'jws verification not implemented in v0' };
79
+ try {
80
+ const signer = await recoverTypedDataAddress({
81
+ domain: RECEIPT_DOMAIN,
82
+ types: RECEIPT_TYPES,
83
+ primaryType: 'Receipt',
84
+ message: receiptMessage(artifact.payload),
85
+ signature: artifact.signature,
86
+ });
87
+ if (expectedSigner && signer.toLowerCase() !== expectedSigner.toLowerCase()) {
88
+ return { valid: false, signer, reason: 'signer does not match expected authorization key' };
89
+ }
90
+ return { valid: true, signer };
91
+ }
92
+ catch (e) {
93
+ return { valid: false, reason: e instanceof Error ? e.message : 'signature recovery failed' };
94
+ }
95
+ }
@@ -0,0 +1,126 @@
1
+ /** Wire types for the merged x402 `offer-receipt` extension (spec: x402-foundation/x402
2
+ * specs/extensions/extension-offer-and-receipt.md, fetched 2026-07-07). The wire shape is
3
+ * declared unstable upstream; everything outside this module treats these as opaque via
4
+ * the codec functions, so upstream churn lands here only. */
5
+ export interface ReceiptPayload {
6
+ version: 1;
7
+ /** CAIP-2, e.g. "eip155:8453" */
8
+ network: string;
9
+ resourceUrl: string;
10
+ payer: string;
11
+ /** unix seconds */
12
+ issuedAt: number;
13
+ /** tx hash, or "" when privacy-minimal (EIP-712 empty-optional rule) */
14
+ transaction: string;
15
+ }
16
+ export interface OfferPayload {
17
+ version: 1;
18
+ resourceUrl: string;
19
+ scheme: string;
20
+ network: string;
21
+ asset: string;
22
+ payTo: string;
23
+ amount: string;
24
+ /** unix seconds; 0 = absent (EIP-712 zero-optional rule) */
25
+ validUntil: number;
26
+ }
27
+ export type SignedArtifact<P> = {
28
+ format: 'eip712';
29
+ payload: P;
30
+ signature: `0x${string}`;
31
+ acceptIndex?: number;
32
+ } | {
33
+ format: 'jws';
34
+ signature: string;
35
+ acceptIndex?: number;
36
+ };
37
+ export type SignedReceipt = SignedArtifact<ReceiptPayload>;
38
+ export type SignedOffer = SignedArtifact<OfferPayload>;
39
+ /** ACP/UCP converged adjustment vocabulary (verified against both specs 2026-07-07).
40
+ * Adopted verbatim so records round-trip card-rail order objects unchanged. */
41
+ export type AdjustmentType = 'refund' | 'return' | 'credit' | 'price_adjustment' | 'dispute' | 'cancellation';
42
+ export type AdjustmentStatus = 'pending' | 'completed' | 'failed';
43
+ export interface Adjustment {
44
+ type: AdjustmentType;
45
+ status: AdjustmentStatus;
46
+ /** minor units, tax-inclusive, as string */
47
+ amount: string;
48
+ currency: string;
49
+ reason?: string;
50
+ /** digest of the receipt/record this adjusts — the hash-chain link */
51
+ adjusts: `0x${string}`;
52
+ }
53
+ /** Tersign compliance record v1 — a SEPARATE artifact bound to the base receipt by digest.
54
+ * The base receipt's EIP-712 schema is fixed upstream; extending it would break signatures,
55
+ * so compliance data composes by reference. MINIMAL tier ≈ EU VAT Art 226b simplified-invoice
56
+ * content (legally sufficient sub-€100); FULL tier adds EN 16931-aligned fields. */
57
+ export interface ComplianceRecordV1 {
58
+ version: 1;
59
+ /** keccak256 of the canonicalized base receipt artifact */
60
+ receiptDigest: `0x${string}`;
61
+ /** sequential per issuer (Art 226(2)); assigned by the ledger when countersigned */
62
+ seq?: number;
63
+ issuedAt: number;
64
+ issuer: {
65
+ name: string;
66
+ /** VAT ID / JP T-number / HK BR no. — labeled by jurisdiction */
67
+ taxId?: string;
68
+ jurisdiction: string;
69
+ };
70
+ buyer?: {
71
+ /** signed principal identity — AP2 mandate subject / deployer, never a bare wallet */
72
+ principal?: string;
73
+ /** AP2 Checkout/Payment-Mandate reference (hash), when present */
74
+ mandateRef?: `0x${string}`;
75
+ };
76
+ /** nature of goods/services (Art 226b(c)) */
77
+ supply: {
78
+ description: string;
79
+ category?: string;
80
+ };
81
+ lines?: Array<{
82
+ description: string;
83
+ quantity: string;
84
+ unitPrice: string;
85
+ net: string;
86
+ }>;
87
+ tax: {
88
+ scheme: 'none' | 'vat' | 'gst' | 'jct' | 'sales';
89
+ currency: string;
90
+ /** total tax, minor units; omit only when scheme = none */
91
+ amount?: string;
92
+ breakdown?: Array<{
93
+ rate: string;
94
+ taxable: string;
95
+ tax: string;
96
+ category?: string;
97
+ }>;
98
+ };
99
+ /** fiat-equivalent valuation of the crypto settlement at issuance (1099-DA / profits-tax) */
100
+ settlement?: {
101
+ fiat: {
102
+ amount: string;
103
+ currency: string;
104
+ source: string;
105
+ asOf: number;
106
+ };
107
+ txHash?: string;
108
+ };
109
+ /** hash-chain to the record this corrects/refunds (Art 226b(e); ViDA corrective-invoice ref) */
110
+ refundOf?: `0x${string}`;
111
+ adjustment?: Adjustment;
112
+ /** retention floor in years; default 7 (HK IRO s.51C ≥ MiCA 5+2) */
113
+ retentionYears: number;
114
+ }
115
+ /** EIP-712-signed attestation over a compliance record. The record itself is JSON (schema may
116
+ * evolve); the attestation schema stays fixed by binding digests only. */
117
+ export interface ComplianceAttestationPayload {
118
+ version: 1;
119
+ recordDigest: `0x${string}`;
120
+ receiptDigest: `0x${string}`;
121
+ issuedAt: number;
122
+ }
123
+ export type SignedComplianceRecord = {
124
+ record: ComplianceRecordV1;
125
+ attestation: SignedArtifact<ComplianceAttestationPayload>;
126
+ };
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Wire types for the merged x402 `offer-receipt` extension (spec: x402-foundation/x402
2
+ * specs/extensions/extension-offer-and-receipt.md, fetched 2026-07-07). The wire shape is
3
+ * declared unstable upstream; everything outside this module treats these as opaque via
4
+ * the codec functions, so upstream churn lands here only. */
5
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /** tersign-verify — third-party receipt verification. No API key, no trust in Tersign:
3
+ * signature recovery is local, and the ledger check only asks the public endpoint whether
4
+ * the counter-signed hash-chain holds.
5
+ *
6
+ * tersign-verify <receipt.json> [--signer 0xseller] [--ledger https://…]
7
+ * tersign-verify <0xdigest> --ledger https://…
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { digestOf } from './canonical.js';
11
+ import { verifyReceipt } from './receipt/eip712.js';
12
+ import { verifyComplianceRecord } from './compliance/record.js';
13
+ function arg(flag) {
14
+ const i = process.argv.indexOf(flag);
15
+ return i > 0 ? process.argv[i + 1] : undefined;
16
+ }
17
+ function fail(msg) {
18
+ console.error(`INVALID: ${msg}`);
19
+ process.exit(1);
20
+ }
21
+ const target = process.argv[2];
22
+ if (!target || target.startsWith('--')) {
23
+ console.error('usage: tersign-verify <receipt.json | 0xdigest> [--signer 0xaddr] [--ledger url]');
24
+ process.exit(2);
25
+ }
26
+ const ledger = arg('--ledger');
27
+ const expectedSigner = arg('--signer');
28
+ async function checkLedger(digest) {
29
+ if (!ledger)
30
+ return;
31
+ const res = await fetch(`${ledger.replace(/\/$/, '')}/v1/receipts/${digest}/verify`);
32
+ const body = (await res.json());
33
+ if (!body.found)
34
+ fail(`ledger has no record of ${digest}`);
35
+ if (!body.chainOk)
36
+ fail('ledger record found but the counter-signed hash-chain does NOT verify');
37
+ console.log(`ledger: counter-signed OK (seller ${body.sellerId}, seq ${body.seq}, ledger key ${body.ledgerSigner})`);
38
+ }
39
+ if (/^0x[0-9a-f]{64}$/i.test(target)) {
40
+ if (!ledger)
41
+ fail('a bare digest can only be checked against a ledger — pass --ledger');
42
+ await checkLedger(target);
43
+ console.log('VALID');
44
+ process.exit(0);
45
+ }
46
+ const parsed = JSON.parse(readFileSync(target, 'utf8'));
47
+ const receipt = 'payload' in parsed || 'signature' in parsed ? parsed : parsed.receipt;
48
+ const record = 'receipt' in parsed ? parsed.record : undefined;
49
+ const result = await verifyReceipt(receipt, expectedSigner);
50
+ if (!result.valid)
51
+ fail(`receipt signature: ${result.reason}`);
52
+ const digest = digestOf(receipt);
53
+ console.log(`signature: OK (signer ${result.signer})`);
54
+ console.log(`digest: ${digest}`);
55
+ if (record) {
56
+ const rec = await verifyComplianceRecord(record, expectedSigner);
57
+ if (!rec.valid)
58
+ fail(`compliance record: ${rec.reason}`);
59
+ if (record.record.receiptDigest !== digest)
60
+ fail('compliance record is bound to a DIFFERENT receipt');
61
+ console.log(`record: OK (bound to receipt, signer ${rec.signer})`);
62
+ }
63
+ await checkLedger(digest);
64
+ console.log('VALID');
package/package.json CHANGED
@@ -1,7 +1,60 @@
1
1
  {
2
2
  "name": "tersign",
3
- "version": "0.0.1",
4
- "description": "Tersign the evidence layer for the agent economy. Name reserved; the SDK lives at @tersign/assure.",
3
+ "version": "0.1.0",
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
- "homepage": "https://github.com/tersignhq"
7
- }
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
21
+ "viem": "^2.21.0",
22
+ "zod": "^3.23.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^26.1.0",
26
+ "typescript": "^5.5.0",
27
+ "vitest": "^2.0.0"
28
+ },
29
+ "bin": {
30
+ "tersign-mcp": "dist/mcp/bin.js",
31
+ "tersign-verify": "dist/verify-bin.js"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./mcp": {
39
+ "types": "./dist/mcp/server.d.ts",
40
+ "import": "./dist/mcp/server.js"
41
+ }
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/tersignhq/tersign-js.git"
46
+ },
47
+ "homepage": "https://tersign-ledger.kevinn-zhang.workers.dev",
48
+ "keywords": [
49
+ "x402",
50
+ "agent-economy",
51
+ "evidence",
52
+ "receipts",
53
+ "eip-712",
54
+ "compliance",
55
+ "disputes",
56
+ "ai-agents",
57
+ "mcp",
58
+ "audit-trail"
59
+ ]
60
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};