tersign 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +53 -3
- package/dist/adapter/x402.d.ts +29 -0
- package/dist/adapter/x402.js +105 -0
- package/dist/assure.d.ts +47 -0
- package/dist/assure.js +68 -0
- package/dist/canonical.d.ts +4 -0
- package/dist/canonical.js +23 -0
- package/dist/compliance/record.d.ts +45 -0
- package/dist/compliance/record.js +94 -0
- package/dist/compliance/types.d.ts +6 -0
- package/dist/compliance/types.js +1 -0
- package/dist/dispute/sign.d.ts +80 -0
- package/dist/dispute/sign.js +212 -0
- package/dist/dispute/types.d.ts +98 -0
- package/dist/dispute/types.js +1 -0
- package/dist/envelope/serialize.d.ts +30 -0
- package/dist/envelope/serialize.js +73 -0
- package/dist/envelope/types.d.ts +49 -0
- package/dist/envelope/types.js +15 -0
- package/dist/evidence/action.d.ts +93 -0
- package/dist/evidence/action.js +70 -0
- package/dist/idempotency/d1.d.ts +32 -0
- package/dist/idempotency/d1.js +46 -0
- package/dist/idempotency/middleware.d.ts +63 -0
- package/dist/idempotency/middleware.js +0 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/ledgerClient.d.ts +34 -0
- package/dist/ledgerClient.js +58 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +5 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +134 -0
- package/dist/mcp/tools.d.ts +57 -0
- package/dist/mcp/tools.js +85 -0
- package/dist/receipt/eip712.d.ts +72 -0
- package/dist/receipt/eip712.js +95 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.js +5 -0
- package/dist/verify-bin.d.ts +2 -0
- package/dist/verify-bin.js +64 -0
- package/package.json +58 -3
- package/index.js +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tersign
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tersign
|
|
2
2
|
|
|
3
|
-
The evidence layer for the agent economy
|
|
3
|
+
**The evidence layer for the agent economy.** When software buys from software, someone has to keep the records straight — this SDK gives agent-commerce sellers counter-signed receipts, tamper-evident action records, and jury-ready evidence envelopes on top of x402 settlement.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```sh
|
|
6
|
+
npm install tersign
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
- **Signed receipts** — implements the merged x402 `offer-receipt` extension (EIP-712), plus **compliance records** (tax/audit-grade fields: EU Art-226b minimal tier / EN 16931 full tier / HK IRO s.51C retention) bound to the base receipt by digest.
|
|
12
|
+
- **Agent action records** — `ActionRecordV1`: digest-bound, GDPR-minimized evidence of agent actions and disclosures, mapped to EU AI Act Art-50 obligations.
|
|
13
|
+
- **Idempotency enforcement** — the x402 `payment-identifier` extension ships the key; this ships the semantics (replay cache, `Idempotent-Replayed`, 409 in-flight/conflict) with pluggable stores (memory, Cloudflare D1).
|
|
14
|
+
- **Refund orchestration** — corrective records hash-chained via `refundOf` (ViDA-style), ACP/UCP adjustment vocabulary verbatim.
|
|
15
|
+
- **Disputes** — signed dispute/evidence/acceptance-criteria artifacts with objective reason codes; deterministic triage upstream of any arbitration venue.
|
|
16
|
+
- **Evidence envelopes** — package any counter-signed record into a jury-ready submission for external venues (Internet Court slot format, Kleros ERC-1497, UMA claims): digests + a public verify URL, never raw evidence, with party statements structurally segregated from ledger-attested content.
|
|
17
|
+
- **Ledger client** — counter-signature + sequential hash-chaining + evidence-pack exports via the hosted Tersign ledger (optional; the SDK works standalone).
|
|
18
|
+
- **MCP server** — `npx tersign-mcp` exposes the full loop (issue / verify / refund / dispute) as Model Context Protocol tools for agent frameworks.
|
|
19
|
+
- **Third-party verification** — `npx tersign-verify <receipt.json | 0xdigest>` recovers signatures and checks the public chain with no account and no trust in Tersign.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
25
|
+
import { Assure, attachToExtensions } from 'tersign';
|
|
26
|
+
|
|
27
|
+
const assure = new Assure({
|
|
28
|
+
signer: privateKeyToAccount(process.env.SELLER_KEY as `0x${string}`),
|
|
29
|
+
issuer: { name: 'Example API Ltd', jurisdiction: 'HK', taxId: 'BR-12345678' },
|
|
30
|
+
// ledger: { url: 'https://tersign-ledger.kevinn-zhang.workers.dev', apiKey: '…', sellerId: '…' },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// after your x402 middleware reports settlement:
|
|
34
|
+
const issued = await assure.issueFor({
|
|
35
|
+
network: 'eip155:8453',
|
|
36
|
+
resourceUrl: 'https://api.example.com/data',
|
|
37
|
+
payer: settlement.payer,
|
|
38
|
+
settledAt: Math.floor(Date.now() / 1000),
|
|
39
|
+
txHash: settlement.transaction,
|
|
40
|
+
supplyDescription: 'Market data, per call',
|
|
41
|
+
});
|
|
42
|
+
return Response.json(attachToExtensions(body, issued));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why not just the official x402 SDK?
|
|
46
|
+
|
|
47
|
+
The official extension gives you the receipt *format*. This gives you the *operation*: replay enforcement (x402 #452 punts it to the app layer), refund records, compliance-grade fields your accountant recognizes, dispute-ready evidence — and third-party verifiability via counter-signed hash chains, so your receipts are exhibits, not testimony.
|
|
48
|
+
|
|
49
|
+
## Verify without trusting anyone
|
|
50
|
+
|
|
51
|
+
Every counter-signed record is publicly checkable: [live ledger + verification](https://tersign-ledger.kevinn-zhang.workers.dev/verify) — no account, no API key. Venues rotate; the transcript endures.
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
v0.1 — EIP-712 receipts, MINIMAL-tier compliance records, action records, disputes v0, evidence envelopes (Internet Court / Kleros / UMA), memory + D1 idempotency stores, MCP server, verify CLI. Wire formats are digest-bound and schema-evolvable; cross-implementation vectors are pinned in CI. MIT.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Assure, SettlementContext } from '../assure.js';
|
|
2
|
+
import { type IdempotencyStore } from '../idempotency/middleware.js';
|
|
3
|
+
export interface SettlementInfo {
|
|
4
|
+
success: boolean;
|
|
5
|
+
transaction?: string | undefined;
|
|
6
|
+
network?: string | undefined;
|
|
7
|
+
payer?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function extractPaymentPayload(headers: Headers): unknown;
|
|
10
|
+
export declare function extractSettlement(headers: Headers): SettlementInfo | undefined;
|
|
11
|
+
export interface WithAssureConfig {
|
|
12
|
+
assure: Assure;
|
|
13
|
+
/** describe the supply for the receipt; defaults to the request path */
|
|
14
|
+
describeSupply?: (req: Request) => string;
|
|
15
|
+
/** override receipt fields derived from the settlement header */
|
|
16
|
+
toSettlementContext?: (req: Request, info: SettlementInfo) => Partial<SettlementContext>;
|
|
17
|
+
clock?: () => number;
|
|
18
|
+
idempotency?: {
|
|
19
|
+
store: IdempotencyStore;
|
|
20
|
+
required: boolean;
|
|
21
|
+
scope: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>;
|
|
25
|
+
/** Wrap an x402-protected handler: enforce idempotency on the way in, issue the signed
|
|
26
|
+
* receipt + compliance record on the way out (only when the settlement header reports
|
|
27
|
+
* success and the response body is JSON). */
|
|
28
|
+
export declare function withAssure(handler: FetchHandler, cfg: WithAssureConfig): FetchHandler;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { attachToExtensions } from '../assure.js';
|
|
2
|
+
import { checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, } from '../idempotency/middleware.js';
|
|
3
|
+
/** Adapter for x402-protected fetch-style handlers ((Request) => Response) — this is the
|
|
4
|
+
* shape of a Hono app (`app.fetch`), a Workers export, and Next.js route handlers, so one
|
|
5
|
+
* wrapper covers the common seller stacks. An adapter pinned to the official x402 SDK's
|
|
6
|
+
* middleware internals is deliberately deferred until we integrate against a pinned
|
|
7
|
+
* version (its surface is still churning); this wrapper only touches the WIRE contract:
|
|
8
|
+
* the payment payload request header and the settlement response header. */
|
|
9
|
+
/** x402 v2 header names, with v1 fallbacks. Re-verify at integration (CLAUDE.md rule 1). */
|
|
10
|
+
const PAYMENT_PAYLOAD_HEADERS = ['payment-signature', 'x-payment'];
|
|
11
|
+
const SETTLEMENT_HEADERS = ['payment-response', 'x-payment-response'];
|
|
12
|
+
function b64json(value) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(atob(value));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function extractPaymentPayload(headers) {
|
|
21
|
+
for (const name of PAYMENT_PAYLOAD_HEADERS) {
|
|
22
|
+
const raw = headers.get(name);
|
|
23
|
+
if (raw)
|
|
24
|
+
return b64json(raw);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function extractSettlement(headers) {
|
|
29
|
+
for (const name of SETTLEMENT_HEADERS) {
|
|
30
|
+
const raw = headers.get(name);
|
|
31
|
+
if (!raw)
|
|
32
|
+
continue;
|
|
33
|
+
const parsed = b64json(raw);
|
|
34
|
+
if (!parsed || typeof parsed !== 'object')
|
|
35
|
+
continue;
|
|
36
|
+
return {
|
|
37
|
+
success: parsed.success === true,
|
|
38
|
+
transaction: str(parsed.transaction) ?? str(parsed.txHash),
|
|
39
|
+
network: str(parsed.network) ?? str(parsed.networkId),
|
|
40
|
+
payer: str(parsed.payer) ?? str(parsed.from),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function str(v) {
|
|
46
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Wrap an x402-protected handler: enforce idempotency on the way in, issue the signed
|
|
49
|
+
* receipt + compliance record on the way out (only when the settlement header reports
|
|
50
|
+
* success and the response body is JSON). */
|
|
51
|
+
export function withAssure(handler, cfg) {
|
|
52
|
+
const now = cfg.clock ?? (() => Math.floor(Date.now() / 1000));
|
|
53
|
+
return async (req) => {
|
|
54
|
+
let onComplete;
|
|
55
|
+
if (cfg.idempotency) {
|
|
56
|
+
const id = extractPaymentId(extractPaymentPayload(req.headers));
|
|
57
|
+
const url = new URL(req.url);
|
|
58
|
+
const fp = fingerprint({ method: req.method, path: url.pathname });
|
|
59
|
+
const outcome = await checkIdempotency(cfg.idempotency, id, fp);
|
|
60
|
+
switch (outcome.kind) {
|
|
61
|
+
case 'missing':
|
|
62
|
+
return Response.json({ error: 'payment-identifier id required' }, { status: 400 });
|
|
63
|
+
case 'conflict':
|
|
64
|
+
return Response.json({ error: 'payment id reused with a different request' }, { status: 409 });
|
|
65
|
+
case 'in-flight':
|
|
66
|
+
return Response.json({ error: 'request with this payment id is in flight' }, { status: 409 });
|
|
67
|
+
case 'replay': {
|
|
68
|
+
const headers = new Headers(outcome.response.headers);
|
|
69
|
+
headers.set(REPLAY_HEADER, 'true');
|
|
70
|
+
return new Response(outcome.response.body, { status: outcome.response.status, headers });
|
|
71
|
+
}
|
|
72
|
+
case 'process':
|
|
73
|
+
onComplete = outcome.onComplete;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let res = await handler(req);
|
|
77
|
+
const settlement = extractSettlement(res.headers);
|
|
78
|
+
if (settlement?.success && (res.headers.get('content-type') ?? '').includes('application/json')) {
|
|
79
|
+
const url = new URL(req.url);
|
|
80
|
+
const overrides = cfg.toSettlementContext?.(req, settlement) ?? {};
|
|
81
|
+
const ctx = {
|
|
82
|
+
network: settlement.network ?? 'eip155:8453',
|
|
83
|
+
resourceUrl: url.origin + url.pathname,
|
|
84
|
+
payer: settlement.payer ?? 'unknown',
|
|
85
|
+
settledAt: now(),
|
|
86
|
+
supplyDescription: cfg.describeSupply?.(req) ?? url.pathname,
|
|
87
|
+
...(settlement.transaction !== undefined ? { txHash: settlement.transaction } : {}),
|
|
88
|
+
...overrides,
|
|
89
|
+
};
|
|
90
|
+
const issued = await cfg.assure.issueFor(ctx);
|
|
91
|
+
const body = (await res.json());
|
|
92
|
+
const decorated = attachToExtensions(body, issued);
|
|
93
|
+
const headers = new Headers(res.headers);
|
|
94
|
+
headers.delete('content-length');
|
|
95
|
+
res = new Response(JSON.stringify(decorated), { status: res.status, headers });
|
|
96
|
+
}
|
|
97
|
+
if (onComplete) {
|
|
98
|
+
const body = await res.clone().text();
|
|
99
|
+
const headerRecord = {};
|
|
100
|
+
res.headers.forEach((v, k) => (headerRecord[k] = v));
|
|
101
|
+
await onComplete({ status: res.status, headers: headerRecord, body });
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/assure.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Account } from 'viem/accounts';
|
|
2
|
+
import { type IssuerConfig } from './compliance/record.js';
|
|
3
|
+
import { type LedgerConfig, type CountersignResult } from './ledgerClient.js';
|
|
4
|
+
import type { ComplianceRecordV1, SignedComplianceRecord, SignedReceipt } from './types.js';
|
|
5
|
+
export interface AssureConfig {
|
|
6
|
+
/** viem account holding the seller's signing key (payTo-key authorization model) */
|
|
7
|
+
signer: Account;
|
|
8
|
+
issuer: IssuerConfig;
|
|
9
|
+
ledger?: LedgerConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface SettlementContext {
|
|
12
|
+
/** CAIP-2 */
|
|
13
|
+
network: string;
|
|
14
|
+
resourceUrl: string;
|
|
15
|
+
payer: string;
|
|
16
|
+
/** unix seconds */
|
|
17
|
+
settledAt: number;
|
|
18
|
+
txHash?: string;
|
|
19
|
+
supplyDescription: string;
|
|
20
|
+
tax?: ComplianceRecordV1['tax'];
|
|
21
|
+
buyer?: ComplianceRecordV1['buyer'];
|
|
22
|
+
fiatValuation?: {
|
|
23
|
+
amount: string;
|
|
24
|
+
currency: string;
|
|
25
|
+
source: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface IssuedReceipt {
|
|
29
|
+
receipt: SignedReceipt;
|
|
30
|
+
compliance: SignedComplianceRecord;
|
|
31
|
+
ledger?: CountersignResult;
|
|
32
|
+
}
|
|
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`. */
|
|
37
|
+
export declare class Assure {
|
|
38
|
+
private cfg;
|
|
39
|
+
private ledger?;
|
|
40
|
+
constructor(cfg: AssureConfig);
|
|
41
|
+
issueFor(ctx: SettlementContext): Promise<IssuedReceipt>;
|
|
42
|
+
}
|
|
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. */
|
|
45
|
+
export declare function attachToExtensions<T extends Record<string, unknown>>(responseBody: T, issued: IssuedReceipt): T & {
|
|
46
|
+
extensions: Record<string, unknown>;
|
|
47
|
+
};
|
package/dist/assure.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { signReceipt } from './receipt/eip712.js';
|
|
2
|
+
import { buildMinimalRecord, signComplianceRecord } from './compliance/record.js';
|
|
3
|
+
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`. */
|
|
8
|
+
export class Assure {
|
|
9
|
+
cfg;
|
|
10
|
+
ledger;
|
|
11
|
+
constructor(cfg) {
|
|
12
|
+
this.cfg = cfg;
|
|
13
|
+
if (cfg.ledger)
|
|
14
|
+
this.ledger = new LedgerClient(cfg.ledger);
|
|
15
|
+
}
|
|
16
|
+
async issueFor(ctx) {
|
|
17
|
+
const payload = {
|
|
18
|
+
version: 1,
|
|
19
|
+
network: ctx.network,
|
|
20
|
+
resourceUrl: ctx.resourceUrl,
|
|
21
|
+
payer: ctx.payer,
|
|
22
|
+
issuedAt: ctx.settledAt,
|
|
23
|
+
transaction: ctx.txHash ?? '',
|
|
24
|
+
};
|
|
25
|
+
const receipt = await signReceipt(payload, this.cfg.signer);
|
|
26
|
+
const input = {
|
|
27
|
+
receipt,
|
|
28
|
+
supplyDescription: ctx.supplyDescription,
|
|
29
|
+
tax: ctx.tax ?? { scheme: 'none', currency: 'USD' },
|
|
30
|
+
issuedAt: ctx.settledAt,
|
|
31
|
+
};
|
|
32
|
+
if (ctx.buyer)
|
|
33
|
+
input.buyer = ctx.buyer;
|
|
34
|
+
if (ctx.fiatValuation) {
|
|
35
|
+
input.settlement = {
|
|
36
|
+
fiat: { ...ctx.fiatValuation, asOf: ctx.settledAt },
|
|
37
|
+
...(ctx.txHash !== undefined ? { txHash: ctx.txHash } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const record = buildMinimalRecord(this.cfg.issuer, input);
|
|
41
|
+
const compliance = await signComplianceRecord(record, this.cfg.signer);
|
|
42
|
+
if (!this.ledger)
|
|
43
|
+
return { receipt, compliance };
|
|
44
|
+
const ledger = await this.ledger.submitReceipt(receipt, compliance);
|
|
45
|
+
return { receipt, compliance, ledger };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
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. */
|
|
50
|
+
export function attachToExtensions(responseBody, issued) {
|
|
51
|
+
const prior = (responseBody.extensions ?? {});
|
|
52
|
+
return {
|
|
53
|
+
...responseBody,
|
|
54
|
+
extensions: {
|
|
55
|
+
...prior,
|
|
56
|
+
'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
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Deterministic JSON: recursively key-sorted, no whitespace. Arrays keep order.
|
|
2
|
+
* undefined properties are dropped (matches JSON.stringify semantics). */
|
|
3
|
+
export declare function canonicalStringify(value: unknown): string;
|
|
4
|
+
export declare function digestOf(value: unknown): `0x${string}`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { keccak256, toBytes } from 'viem';
|
|
2
|
+
/** Deterministic JSON: recursively key-sorted, no whitespace. Arrays keep order.
|
|
3
|
+
* undefined properties are dropped (matches JSON.stringify semantics). */
|
|
4
|
+
export function canonicalStringify(value) {
|
|
5
|
+
return JSON.stringify(sortValue(value));
|
|
6
|
+
}
|
|
7
|
+
function sortValue(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map(sortValue);
|
|
10
|
+
if (value !== null && typeof value === 'object') {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const key of Object.keys(value).sort()) {
|
|
13
|
+
const v = value[key];
|
|
14
|
+
if (v !== undefined)
|
|
15
|
+
out[key] = sortValue(v);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function digestOf(value) {
|
|
22
|
+
return keccak256(toBytes(canonicalStringify(value)));
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Account } from 'viem/accounts';
|
|
2
|
+
import type { Adjustment, ComplianceRecordV1, SignedComplianceRecord, SignedReceipt, VerifyLike } from './types.js';
|
|
3
|
+
export declare const COMPLIANCE_DOMAIN: {
|
|
4
|
+
readonly name: "tersign compliance-record";
|
|
5
|
+
readonly version: "1";
|
|
6
|
+
readonly chainId: 1n;
|
|
7
|
+
};
|
|
8
|
+
export declare const COMPLIANCE_TYPES: {
|
|
9
|
+
readonly ComplianceAttestation: readonly [{
|
|
10
|
+
readonly name: "version";
|
|
11
|
+
readonly type: "uint256";
|
|
12
|
+
}, {
|
|
13
|
+
readonly name: "recordDigest";
|
|
14
|
+
readonly type: "bytes32";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "receiptDigest";
|
|
17
|
+
readonly type: "bytes32";
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "issuedAt";
|
|
20
|
+
readonly type: "uint256";
|
|
21
|
+
}];
|
|
22
|
+
};
|
|
23
|
+
export interface IssuerConfig {
|
|
24
|
+
name: string;
|
|
25
|
+
jurisdiction: string;
|
|
26
|
+
taxId?: string;
|
|
27
|
+
/** default 7 — HK IRO s.51C floor, ≥ MiCA 5+2 */
|
|
28
|
+
retentionYears?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface MinimalRecordInput {
|
|
31
|
+
receipt: SignedReceipt;
|
|
32
|
+
supplyDescription: string;
|
|
33
|
+
tax: ComplianceRecordV1['tax'];
|
|
34
|
+
issuedAt: number;
|
|
35
|
+
buyer?: ComplianceRecordV1['buyer'];
|
|
36
|
+
settlement?: ComplianceRecordV1['settlement'];
|
|
37
|
+
refundOf?: `0x${string}`;
|
|
38
|
+
adjustment?: Adjustment;
|
|
39
|
+
}
|
|
40
|
+
/** MINIMAL tier ≈ EU VAT Art 226b simplified-invoice content — legally sufficient for
|
|
41
|
+
* sub-€100 supplies EU-wide, and the default for machine-to-machine micro-receipts. */
|
|
42
|
+
export declare function buildMinimalRecord(issuer: IssuerConfig, input: MinimalRecordInput): ComplianceRecordV1;
|
|
43
|
+
export declare function recordDigest(record: ComplianceRecordV1): `0x${string}`;
|
|
44
|
+
export declare function signComplianceRecord(record: ComplianceRecordV1, account: Account): Promise<SignedComplianceRecord>;
|
|
45
|
+
export declare function verifyComplianceRecord(signed: SignedComplianceRecord, expectedSigner?: string): Promise<VerifyLike>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { recoverTypedDataAddress } from 'viem';
|
|
2
|
+
import { digestOf } from '../canonical.js';
|
|
3
|
+
export const COMPLIANCE_DOMAIN = { name: 'tersign compliance-record', version: '1', chainId: 1n };
|
|
4
|
+
export const COMPLIANCE_TYPES = {
|
|
5
|
+
ComplianceAttestation: [
|
|
6
|
+
{ name: 'version', type: 'uint256' },
|
|
7
|
+
{ name: 'recordDigest', type: 'bytes32' },
|
|
8
|
+
{ name: 'receiptDigest', type: 'bytes32' },
|
|
9
|
+
{ name: 'issuedAt', type: 'uint256' },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
/** MINIMAL tier ≈ EU VAT Art 226b simplified-invoice content — legally sufficient for
|
|
13
|
+
* sub-€100 supplies EU-wide, and the default for machine-to-machine micro-receipts. */
|
|
14
|
+
export function buildMinimalRecord(issuer, input) {
|
|
15
|
+
const record = {
|
|
16
|
+
version: 1,
|
|
17
|
+
receiptDigest: digestOf(input.receipt),
|
|
18
|
+
issuedAt: input.issuedAt,
|
|
19
|
+
issuer: {
|
|
20
|
+
name: issuer.name,
|
|
21
|
+
jurisdiction: issuer.jurisdiction,
|
|
22
|
+
...(issuer.taxId !== undefined ? { taxId: issuer.taxId } : {}),
|
|
23
|
+
},
|
|
24
|
+
supply: { description: input.supplyDescription },
|
|
25
|
+
tax: input.tax,
|
|
26
|
+
retentionYears: issuer.retentionYears ?? 7,
|
|
27
|
+
};
|
|
28
|
+
if (input.buyer)
|
|
29
|
+
record.buyer = input.buyer;
|
|
30
|
+
if (input.settlement)
|
|
31
|
+
record.settlement = input.settlement;
|
|
32
|
+
if (input.refundOf)
|
|
33
|
+
record.refundOf = input.refundOf;
|
|
34
|
+
if (input.adjustment)
|
|
35
|
+
record.adjustment = input.adjustment;
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
export function recordDigest(record) {
|
|
39
|
+
return digestOf(record);
|
|
40
|
+
}
|
|
41
|
+
export async function signComplianceRecord(record, account) {
|
|
42
|
+
if (!account.signTypedData)
|
|
43
|
+
throw new Error('account cannot sign typed data');
|
|
44
|
+
const payload = {
|
|
45
|
+
version: 1,
|
|
46
|
+
recordDigest: recordDigest(record),
|
|
47
|
+
receiptDigest: record.receiptDigest,
|
|
48
|
+
issuedAt: record.issuedAt,
|
|
49
|
+
};
|
|
50
|
+
const signature = await account.signTypedData({
|
|
51
|
+
domain: COMPLIANCE_DOMAIN,
|
|
52
|
+
types: COMPLIANCE_TYPES,
|
|
53
|
+
primaryType: 'ComplianceAttestation',
|
|
54
|
+
message: {
|
|
55
|
+
version: BigInt(payload.version),
|
|
56
|
+
recordDigest: payload.recordDigest,
|
|
57
|
+
receiptDigest: payload.receiptDigest,
|
|
58
|
+
issuedAt: BigInt(payload.issuedAt),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return { record, attestation: { format: 'eip712', payload, signature } };
|
|
62
|
+
}
|
|
63
|
+
export async function verifyComplianceRecord(signed, expectedSigner) {
|
|
64
|
+
const { attestation, record } = signed;
|
|
65
|
+
if (attestation.format !== 'eip712')
|
|
66
|
+
return { valid: false, reason: 'jws not implemented in v0' };
|
|
67
|
+
if (attestation.payload.recordDigest !== recordDigest(record)) {
|
|
68
|
+
return { valid: false, reason: 'record digest mismatch — record was altered after signing' };
|
|
69
|
+
}
|
|
70
|
+
if (attestation.payload.receiptDigest !== record.receiptDigest) {
|
|
71
|
+
return { valid: false, reason: 'attestation/receipt digest mismatch' };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const signer = await recoverTypedDataAddress({
|
|
75
|
+
domain: COMPLIANCE_DOMAIN,
|
|
76
|
+
types: COMPLIANCE_TYPES,
|
|
77
|
+
primaryType: 'ComplianceAttestation',
|
|
78
|
+
message: {
|
|
79
|
+
version: BigInt(attestation.payload.version),
|
|
80
|
+
recordDigest: attestation.payload.recordDigest,
|
|
81
|
+
receiptDigest: attestation.payload.receiptDigest,
|
|
82
|
+
issuedAt: BigInt(attestation.payload.issuedAt),
|
|
83
|
+
},
|
|
84
|
+
signature: attestation.signature,
|
|
85
|
+
});
|
|
86
|
+
if (expectedSigner && signer.toLowerCase() !== expectedSigner.toLowerCase()) {
|
|
87
|
+
return { valid: false, signer, reason: 'unexpected signer' };
|
|
88
|
+
}
|
|
89
|
+
return { valid: true, signer };
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return { valid: false, reason: e instanceof Error ? e.message : 'signature recovery failed' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Account } from 'viem/accounts';
|
|
2
|
+
import type { AcceptanceCriteriaV1, DisputePayloadV1, EvidencePayloadV1, SignedCriteria, SignedDispute, SignedEvidence, VerifyLike } from './types.js';
|
|
3
|
+
export declare const DISPUTE_DOMAIN: {
|
|
4
|
+
readonly name: "tersign dispute";
|
|
5
|
+
readonly version: "1";
|
|
6
|
+
readonly chainId: 1n;
|
|
7
|
+
};
|
|
8
|
+
export declare const EVIDENCE_DOMAIN: {
|
|
9
|
+
readonly name: "tersign evidence";
|
|
10
|
+
readonly version: "1";
|
|
11
|
+
readonly chainId: 1n;
|
|
12
|
+
};
|
|
13
|
+
export declare const CRITERIA_DOMAIN: {
|
|
14
|
+
readonly name: "tersign acceptance-criteria";
|
|
15
|
+
readonly version: "1";
|
|
16
|
+
readonly chainId: 1n;
|
|
17
|
+
};
|
|
18
|
+
export declare const DISPUTE_TYPES: {
|
|
19
|
+
readonly DisputeAttestation: readonly [{
|
|
20
|
+
readonly name: "version";
|
|
21
|
+
readonly type: "uint256";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "disputeDigest";
|
|
24
|
+
readonly type: "bytes32";
|
|
25
|
+
}, {
|
|
26
|
+
readonly name: "receiptDigest";
|
|
27
|
+
readonly type: "bytes32";
|
|
28
|
+
}, {
|
|
29
|
+
readonly name: "openedAt";
|
|
30
|
+
readonly type: "uint256";
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
33
|
+
export declare const EVIDENCE_TYPES: {
|
|
34
|
+
readonly EvidenceAttestation: readonly [{
|
|
35
|
+
readonly name: "version";
|
|
36
|
+
readonly type: "uint256";
|
|
37
|
+
}, {
|
|
38
|
+
readonly name: "evidenceDigest";
|
|
39
|
+
readonly type: "bytes32";
|
|
40
|
+
}, {
|
|
41
|
+
readonly name: "disputeDigest";
|
|
42
|
+
readonly type: "bytes32";
|
|
43
|
+
}, {
|
|
44
|
+
readonly name: "submittedAt";
|
|
45
|
+
readonly type: "uint256";
|
|
46
|
+
}];
|
|
47
|
+
};
|
|
48
|
+
export declare const CRITERIA_TYPES: {
|
|
49
|
+
readonly CriteriaAttestation: readonly [{
|
|
50
|
+
readonly name: "version";
|
|
51
|
+
readonly type: "uint256";
|
|
52
|
+
}, {
|
|
53
|
+
readonly name: "criteriaDigest";
|
|
54
|
+
readonly type: "bytes32";
|
|
55
|
+
}, {
|
|
56
|
+
readonly name: "issuedAt";
|
|
57
|
+
readonly type: "uint256";
|
|
58
|
+
}];
|
|
59
|
+
};
|
|
60
|
+
/** Pinned digest of the dispute-layer EIP-712 material. The ledger re-declares these
|
|
61
|
+
* constants (Workers bundle, no shared package yet) and pins the SAME vector — if either
|
|
62
|
+
* side edits a domain or type, the cross-impl test breaks before signatures do. */
|
|
63
|
+
export declare const DISPUTE_WIRE_VECTOR: `0x${string}`;
|
|
64
|
+
export declare function disputeDigest(dispute: DisputePayloadV1): `0x${string}`;
|
|
65
|
+
export declare function evidenceDigest(evidence: EvidencePayloadV1): `0x${string}`;
|
|
66
|
+
export declare function criteriaDigest(criteria: AcceptanceCriteriaV1): `0x${string}`;
|
|
67
|
+
/** Sign a dispute as the CLAIMANT. The ledger only accepts disputes whose recovered
|
|
68
|
+
* signer equals the disputed receipt's `payer` — possession of the paying key IS the
|
|
69
|
+
* standing to dispute. */
|
|
70
|
+
export declare function signDispute(dispute: DisputePayloadV1, account: Account): Promise<SignedDispute>;
|
|
71
|
+
export declare function verifyDispute(signed: SignedDispute, expectedSigner?: string): Promise<VerifyLike>;
|
|
72
|
+
/** Sign evidence as either party. Role/identity binding is enforced by the ledger:
|
|
73
|
+
* claimant evidence must recover to the receipt payer, respondent evidence to the
|
|
74
|
+
* seller's payTo key. */
|
|
75
|
+
export declare function signEvidence(evidence: EvidencePayloadV1, account: Account): Promise<SignedEvidence>;
|
|
76
|
+
export declare function verifyEvidence(signed: SignedEvidence, expectedSigner?: string): Promise<VerifyLike>;
|
|
77
|
+
/** Sign acceptance criteria as the SELLER, pre-committing the machine-checkable bar the
|
|
78
|
+
* arbiter will hold the delivery to. Attach the returned digest to offers/records. */
|
|
79
|
+
export declare function signCriteria(criteria: AcceptanceCriteriaV1, account: Account): Promise<SignedCriteria>;
|
|
80
|
+
export declare function verifyCriteria(signed: SignedCriteria, expectedSigner?: string): Promise<VerifyLike>;
|