tersign 0.5.0 → 0.6.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.
- package/README.md +20 -7
- package/dist/adapter/x402.d.ts +59 -3
- package/dist/adapter/x402.js +168 -34
- package/dist/assure.d.ts +69 -7
- package/dist/assure.js +74 -16
- package/dist/compliance/record.d.ts +11 -5
- package/dist/compliance/record.js +10 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/ledgerClient.d.ts +3 -0
- package/dist/ledgerClient.js +2 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +8 -8
- package/dist/mcp/tools.d.ts +3 -2
- package/dist/mcp/tools.js +2 -2
- package/dist/receipt/binding.d.ts +4 -1
- package/dist/receipt/binding.js +4 -1
- package/dist/receipt/network.d.ts +31 -0
- package/dist/receipt/network.js +71 -0
- package/dist/types.d.ts +25 -12
- package/dist/types.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
## Verify a Real Entry — Right Now
|
|
19
19
|
|
|
20
|
-
No account. No API key. This is the genesis receipt, `seq 1` on the production chain:
|
|
20
|
+
No account. No API key. This is the genesis receipt, `seq 1` on the production chain. It is a self-signed demo: Tersign signed it as seller and named its own key as payer, so its EIP-712 payload signature (domain `{name: "x402 receipt", version: "1", chainId: 1}`, per the x402 offer-and-receipt extension) recovers to the payload's own payer, `0x36f82906859E5B0bd076069f8cdfAea355358b14`. [`/v1/genesis`](https://tersign.ai/v1/genesis) serves the record with the recipe for each of its signatures.
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
23
|
npx tersign verify 0xe5874f1ffe87f0a6dd9eb157730f67b86ee4538b125fe30fcc4e165213dd3fc4
|
|
@@ -69,7 +69,7 @@ graph LR
|
|
|
69
69
|
|
|
70
70
|
<sub>Diagram renders on GitHub. On npm, the paragraph above IS the diagram.</sub>
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
A refund record references the record it corrects by digest (`refundOf`). Disputes attach to the digest with objective reason codes. Party statements are structurally segregated behind an `UNVERIFIED` marker — the evidence stays prompt-injection-hardened.
|
|
73
73
|
|
|
74
74
|
## Enter the Record
|
|
75
75
|
|
|
@@ -77,15 +77,28 @@ Refunds chain back to the original receipt via `refundOf`. Disputes attach to th
|
|
|
77
77
|
npm i tersign
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
`withAssure()` wraps your x402 fetch handler
|
|
80
|
+
`withAssure()` wraps your x402 fetch handler. On a settled call it signs a receipt and a compliance-fields record with your key and merges both into the `PAYMENT-RESPONSE` header, the base64 JSON settlement response x402 wallets already read; the response body is left alone. When it cannot issue them (a settlement network that is neither CAIP-2 nor a v1 name in its table, such as `"bsc"`, or a ledger error), the paid response goes out exactly as your handler returned it, with no receipt, and the error goes to `onError` (default: `console.error`); with idempotency on, a retry with the same payment id replays that response. On a 402 it adds `compliance-fields` to the `PAYMENT-REQUIRED` header. When the `Assure` is built with a ledger, the ledger counter-signs the receipt into your chain; 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. Without a ledger, both carry your signature alone.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
84
|
+
import { Assure, withAssure } from 'tersign';
|
|
85
|
+
|
|
86
|
+
const signer = privateKeyToAccount(process.env.TERSIGN_SELLER_KEY as `0x${string}`);
|
|
87
|
+
const assure = new Assure({ signer, issuer: { name: 'Example API Ltd', jurisdiction: 'HK' } });
|
|
88
|
+
export default { fetch: withAssure(app.fetch, { assure }) }; // app: your x402-protected Hono app
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A wallet finds the receipt at `extensions["offer-receipt"].info.receipt` and the record at `extensions["compliance-fields"].info.record` of the decoded `PAYMENT-RESPONSE` (`X-PAYMENT-RESPONSE` on x402 v1). Sellers whose readers still parse the JSON body can pass `legacyBodyPlacement: true` for one more minor release.
|
|
92
|
+
|
|
93
|
+
The full register:
|
|
81
94
|
|
|
82
95
|
| Capability | In the record |
|
|
83
96
|
|---|---|
|
|
84
97
|
| Receipts | Seller-signed EIP-712 (x402 offer-receipt extension), keccak256 canonical digests |
|
|
85
|
-
| `withAssure()` | x402 fetch-handler adapter —
|
|
86
|
-
| Compliance
|
|
98
|
+
| `withAssure()` | x402 fetch-handler adapter — receipt + compliance-fields record in `PAYMENT-RESPONSE` on each paid call it can issue for, the paid response passed through unchanged when it cannot; `compliance-fields` advertised on 402 |
|
|
99
|
+
| Compliance-fields records | MINIMAL tier (Art 226b simplified-invoice content) via `buildMinimalRecord` · ledger exports `format=art226b` · `format=s51c` — mappings, not certifications |
|
|
87
100
|
| Action records | `ActionRecordV1` — GDPR-minimized; captures the content of an Art-50 disclosure so the disclosure itself is independently attested, not self-reported |
|
|
88
|
-
| Refunds |
|
|
101
|
+
| Refunds | A refund record carries `refundOf`, the digest of the record it corrects · `record_refund` logs a pending refund entry on the ledger (not counter-signed) |
|
|
89
102
|
| Disputes v0 | Objective reason codes, evidence submission, adjudication |
|
|
90
103
|
| Venue envelopes | Internet Court (5,000-char slot) · Kleros ERC-1497 · UMA · generic |
|
|
91
104
|
| Evidence packs | `format=art50` · `format=safr` (beta) |
|
|
@@ -168,7 +181,7 @@ Full URLs, readable without auth. If you are an agent, start here.
|
|
|
168
181
|
| Bundle verifier, out-of-band | https://tersign.ai/verify/v1/ — `verify_bundle.py` · `keccak.py` · `secp256k1.py` · `SHA256SUMS`. A bundle ships its own checker; for evidence from an interested party fetch this copy and diff the two. |
|
|
169
182
|
| llms.txt | https://raw.githubusercontent.com/tersignhq/tersign-js/main/llms.txt |
|
|
170
183
|
| Conformance vectors (RFC 8785 + keccak256, two-sided) | https://github.com/tersignhq/evidence-record-conformance |
|
|
171
|
-
| Sample
|
|
184
|
+
| Sample compliance-fields record + digests | https://github.com/tersignhq/tersign-js/blob/main/test/fixtures/compliance-record.json |
|
|
172
185
|
| Genesis verify | `npx tersign verify 0xe5874f1ffe87f0a6dd9eb157730f67b86ee4538b125fe30fcc4e165213dd3fc4` |
|
|
173
186
|
|
|
174
187
|
---
|
package/dist/adapter/x402.d.ts
CHANGED
|
@@ -6,8 +6,42 @@ export interface SettlementInfo {
|
|
|
6
6
|
network?: string | undefined;
|
|
7
7
|
payer?: string | undefined;
|
|
8
8
|
}
|
|
9
|
+
/** Encode a JSON value the way an x402 HTTP header carries it (base64 over UTF-8 JSON). */
|
|
10
|
+
export declare function encodeX402Header(value: unknown): string;
|
|
11
|
+
/** Decode an x402 HTTP header value; `undefined` when it is not base64 over UTF-8 JSON. */
|
|
12
|
+
export declare function decodeX402Header(value: string): unknown;
|
|
9
13
|
export declare function extractPaymentPayload(headers: Headers): unknown;
|
|
10
14
|
export declare function extractSettlement(headers: Headers): SettlementInfo | undefined;
|
|
15
|
+
/** What a 402 advertises under `extensions["compliance-fields"].info`. Each member is a claim
|
|
16
|
+
* about the records this server emits, so set one only when it is true of every record:
|
|
17
|
+
* `tiers: ['minimal']` needs every record to carry each MINIMAL member, including `tax.amount`
|
|
18
|
+
* whenever `tax.scheme` is not `none`. The default advertises the extension with no tier or
|
|
19
|
+
* jurisdiction claim. */
|
|
20
|
+
export interface ComplianceAdvertisement {
|
|
21
|
+
tiers?: ReadonlyArray<'minimal' | 'full'>;
|
|
22
|
+
jurisdictions?: ReadonlyArray<string>;
|
|
23
|
+
}
|
|
24
|
+
/** JSON Schema for the advertised `info` (the core v2 spec makes `schema` a required member of
|
|
25
|
+
* every PaymentRequired extension entry). */
|
|
26
|
+
export declare const COMPLIANCE_FIELDS_ADVERTISEMENT_SCHEMA: {
|
|
27
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
28
|
+
readonly type: "object";
|
|
29
|
+
readonly properties: {
|
|
30
|
+
readonly tiers: {
|
|
31
|
+
readonly type: "array";
|
|
32
|
+
readonly items: {
|
|
33
|
+
readonly type: "string";
|
|
34
|
+
readonly enum: readonly ["minimal", "full"];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
readonly jurisdictions: {
|
|
38
|
+
readonly type: "array";
|
|
39
|
+
readonly items: {
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
11
45
|
export interface WithAssureConfig {
|
|
12
46
|
assure: Assure;
|
|
13
47
|
/** describe the supply for the receipt; defaults to the request path */
|
|
@@ -20,10 +54,32 @@ export interface WithAssureConfig {
|
|
|
20
54
|
required: boolean;
|
|
21
55
|
scope: string;
|
|
22
56
|
};
|
|
57
|
+
/** Advertise `compliance-fields` in a 402's `PAYMENT-REQUIRED` header (x402 v2). Default: on,
|
|
58
|
+
* with an empty claim set. `false` leaves 402 responses untouched. An entry the handler
|
|
59
|
+
* already put there is kept as it is. */
|
|
60
|
+
advertise?: false | ComplianceAdvertisement;
|
|
61
|
+
/** @deprecated Also decorate a JSON response BODY with the receipt and record (the ≤0.5
|
|
62
|
+
* placement). The `PAYMENT-RESPONSE` header carries them either way. Scheduled for removal in
|
|
63
|
+
* the next minor release after the one that introduced header placement. A body that does not
|
|
64
|
+
* parse as a JSON object is left as it is. */
|
|
65
|
+
legacyBodyPlacement?: boolean;
|
|
66
|
+
/** Called when a settled call's receipt cannot be issued: a settlement network that is neither
|
|
67
|
+
* CAIP-2 nor in the v1 table (`toCaip2Network`), a ledger error, a throwing `toSettlementContext`
|
|
68
|
+
* or `describeSupply`. The paid response then goes out exactly as the handler returned it, with
|
|
69
|
+
* no receipt or record in it. Default: one `console.error` line per failure. An error thrown by
|
|
70
|
+
* this callback, or a rejection of a promise it returns, is ignored. */
|
|
71
|
+
onError?: (err: unknown) => void;
|
|
23
72
|
}
|
|
24
73
|
type FetchHandler = (req: Request) => Response | Promise<Response>;
|
|
25
|
-
/** Wrap an x402-protected handler: enforce idempotency on the way in
|
|
26
|
-
*
|
|
27
|
-
*
|
|
74
|
+
/** Wrap an x402-protected handler: enforce idempotency on the way in; on the way out, advertise
|
|
75
|
+
* `compliance-fields` on a 402, and — when the settlement header reports success — issue the
|
|
76
|
+
* signed receipt + compliance-fields record and merge both into that same settlement header
|
|
77
|
+
* (`PAYMENT-RESPONSE`, or `X-PAYMENT-RESPONSE` for v1), where x402 wallets read them. The
|
|
78
|
+
* response body is left untouched unless `legacyBodyPlacement` is set.
|
|
79
|
+
*
|
|
80
|
+
* Once the settlement header reports success the buyer has paid, so a failure to issue does not
|
|
81
|
+
* throw: the response goes out exactly as the handler returned it, the error goes to `onError`,
|
|
82
|
+
* and with idempotency on the payment id is completed with that delivered response, so a retry
|
|
83
|
+
* replays it instead of finding the id stuck in flight. */
|
|
28
84
|
export declare function withAssure(handler: FetchHandler, cfg: WithAssureConfig): FetchHandler;
|
|
29
85
|
export {};
|
package/dist/adapter/x402.js
CHANGED
|
@@ -1,22 +1,57 @@
|
|
|
1
|
-
import { attachToExtensions } from '../assure.js';
|
|
1
|
+
import { attachToExtensions, attachToSettlementResponse } from '../assure.js';
|
|
2
2
|
import { checkIdempotency, extractPaymentId, fingerprint, REPLAY_HEADER, } from '../idempotency/middleware.js';
|
|
3
3
|
/** Adapter for x402-protected fetch-style handlers ((Request) => Response) — this is the
|
|
4
4
|
* shape of a Hono app (`app.fetch`), a Workers export, and Next.js route handlers, so one
|
|
5
5
|
* wrapper covers the common seller stacks. An adapter pinned to the official x402 SDK's
|
|
6
6
|
* middleware internals is deliberately deferred until we integrate against a pinned
|
|
7
7
|
* version (its surface is still churning); this wrapper only touches the WIRE contract:
|
|
8
|
-
* the payment payload request header and the settlement
|
|
9
|
-
|
|
8
|
+
* the payment payload request header, the payment-required header and the settlement
|
|
9
|
+
* response header. */
|
|
10
|
+
/** x402 HTTP transport headers, v2 names first with v1 fallbacks. Verified 2026-09-29 against
|
|
11
|
+
* x402-foundation/x402 specs/transports-v2/http.md (main 5eee1e3c35): `PAYMENT-REQUIRED`
|
|
12
|
+
* (server → client, PaymentRequired), `PAYMENT-SIGNATURE` (client → server, PaymentPayload),
|
|
13
|
+
* `PAYMENT-RESPONSE` (server → client, SettlementResponse), each base64 over UTF-8 JSON; "All
|
|
14
|
+
* x402 protocol information is communicated through headers". Re-verify at integration
|
|
15
|
+
* (CLAUDE.md rule 1). */
|
|
10
16
|
const PAYMENT_PAYLOAD_HEADERS = ['payment-signature', 'x-payment'];
|
|
11
17
|
const SETTLEMENT_HEADERS = ['payment-response', 'x-payment-response'];
|
|
18
|
+
const PAYMENT_REQUIRED_HEADER = 'payment-required';
|
|
19
|
+
/** base64 → UTF-8 text, as the upstream reference decodes (`safeBase64Decode`), so a record
|
|
20
|
+
* carrying a non-ASCII issuer name or supply description survives the header round trip. Unlike
|
|
21
|
+
* the reference, invalid UTF-8 is refused rather than replaced with U+FFFD: a header this
|
|
22
|
+
* wrapper rewrites must decode exactly, or it is left alone. */
|
|
23
|
+
function b64decodeUtf8(value) {
|
|
24
|
+
const binary = atob(value);
|
|
25
|
+
const bytes = new Uint8Array(binary.length);
|
|
26
|
+
for (let i = 0; i < binary.length; i++)
|
|
27
|
+
bytes[i] = binary.charCodeAt(i);
|
|
28
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
29
|
+
}
|
|
30
|
+
/** UTF-8 text → base64, as the upstream reference encodes (`safeBase64Encode`). Plain `btoa`
|
|
31
|
+
* throws on any character above U+00FF. */
|
|
32
|
+
function b64encodeUtf8(text) {
|
|
33
|
+
const bytes = new TextEncoder().encode(text);
|
|
34
|
+
let binary = '';
|
|
35
|
+
for (const b of bytes)
|
|
36
|
+
binary += String.fromCharCode(b);
|
|
37
|
+
return btoa(binary);
|
|
38
|
+
}
|
|
12
39
|
function b64json(value) {
|
|
13
40
|
try {
|
|
14
|
-
return JSON.parse(
|
|
41
|
+
return JSON.parse(b64decodeUtf8(value));
|
|
15
42
|
}
|
|
16
43
|
catch {
|
|
17
44
|
return undefined;
|
|
18
45
|
}
|
|
19
46
|
}
|
|
47
|
+
/** Encode a JSON value the way an x402 HTTP header carries it (base64 over UTF-8 JSON). */
|
|
48
|
+
export function encodeX402Header(value) {
|
|
49
|
+
return b64encodeUtf8(JSON.stringify(value));
|
|
50
|
+
}
|
|
51
|
+
/** Decode an x402 HTTP header value; `undefined` when it is not base64 over UTF-8 JSON. */
|
|
52
|
+
export function decodeX402Header(value) {
|
|
53
|
+
return b64json(value);
|
|
54
|
+
}
|
|
20
55
|
export function extractPaymentPayload(headers) {
|
|
21
56
|
for (const name of PAYMENT_PAYLOAD_HEADERS) {
|
|
22
57
|
const raw = headers.get(name);
|
|
@@ -25,29 +60,105 @@ export function extractPaymentPayload(headers) {
|
|
|
25
60
|
}
|
|
26
61
|
return undefined;
|
|
27
62
|
}
|
|
28
|
-
|
|
63
|
+
function findSettlement(headers) {
|
|
29
64
|
for (const name of SETTLEMENT_HEADERS) {
|
|
30
65
|
const raw = headers.get(name);
|
|
31
66
|
if (!raw)
|
|
32
67
|
continue;
|
|
33
68
|
const parsed = b64json(raw);
|
|
34
|
-
if (!parsed
|
|
69
|
+
if (!isPlainRecord(parsed))
|
|
35
70
|
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
|
-
};
|
|
71
|
+
return { name, response: parsed };
|
|
42
72
|
}
|
|
43
73
|
return undefined;
|
|
44
74
|
}
|
|
75
|
+
export function extractSettlement(headers) {
|
|
76
|
+
const found = findSettlement(headers);
|
|
77
|
+
return found ? settlementInfo(found.response) : undefined;
|
|
78
|
+
}
|
|
79
|
+
function settlementInfo(parsed) {
|
|
80
|
+
return {
|
|
81
|
+
success: parsed.success === true,
|
|
82
|
+
transaction: str(parsed.transaction) ?? str(parsed.txHash),
|
|
83
|
+
network: str(parsed.network) ?? str(parsed.networkId),
|
|
84
|
+
payer: str(parsed.payer) ?? str(parsed.from),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
45
87
|
function str(v) {
|
|
46
88
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
47
89
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
function isPlainRecord(v) {
|
|
91
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
92
|
+
}
|
|
93
|
+
/** JSON Schema for the advertised `info` (the core v2 spec makes `schema` a required member of
|
|
94
|
+
* every PaymentRequired extension entry). */
|
|
95
|
+
export const COMPLIANCE_FIELDS_ADVERTISEMENT_SCHEMA = {
|
|
96
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
tiers: { type: 'array', items: { type: 'string', enum: ['minimal', 'full'] } },
|
|
100
|
+
jurisdictions: { type: 'array', items: { type: 'string' } },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
function reportIssueError(err) {
|
|
104
|
+
let message;
|
|
105
|
+
try {
|
|
106
|
+
message = String(err instanceof Error ? err.message : err);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
message = 'unprintable error';
|
|
110
|
+
}
|
|
111
|
+
console.error(`tersign withAssure: receipt not issued, response delivered unchanged: ${message.replace(/\s+/g, ' ')}`);
|
|
112
|
+
}
|
|
113
|
+
/** The legacy body placement reads a clone, so a body that is not a JSON object stays readable
|
|
114
|
+
* and goes out as it came. */
|
|
115
|
+
async function jsonObjectBody(res) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = await res.clone().json();
|
|
118
|
+
return isPlainRecord(parsed) ? parsed : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function withHeaders(res, headers, body = res.body) {
|
|
125
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers });
|
|
126
|
+
}
|
|
127
|
+
function advertiseCompliance(res, adv) {
|
|
128
|
+
if (res.status !== 402)
|
|
129
|
+
return res;
|
|
130
|
+
const raw = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
131
|
+
if (!raw)
|
|
132
|
+
return res;
|
|
133
|
+
const required = b64json(raw);
|
|
134
|
+
if (!isPlainRecord(required))
|
|
135
|
+
return res;
|
|
136
|
+
const prior = isPlainRecord(required.extensions) ? required.extensions : {};
|
|
137
|
+
if (prior['compliance-fields'] !== undefined)
|
|
138
|
+
return res;
|
|
139
|
+
const info = {};
|
|
140
|
+
if (adv.tiers !== undefined)
|
|
141
|
+
info.tiers = [...adv.tiers];
|
|
142
|
+
if (adv.jurisdictions !== undefined)
|
|
143
|
+
info.jurisdictions = [...adv.jurisdictions];
|
|
144
|
+
const decorated = {
|
|
145
|
+
...required,
|
|
146
|
+
extensions: { ...prior, 'compliance-fields': { info, schema: COMPLIANCE_FIELDS_ADVERTISEMENT_SCHEMA } },
|
|
147
|
+
};
|
|
148
|
+
const headers = new Headers(res.headers);
|
|
149
|
+
headers.set(PAYMENT_REQUIRED_HEADER, encodeX402Header(decorated));
|
|
150
|
+
return withHeaders(res, headers);
|
|
151
|
+
}
|
|
152
|
+
/** Wrap an x402-protected handler: enforce idempotency on the way in; on the way out, advertise
|
|
153
|
+
* `compliance-fields` on a 402, and — when the settlement header reports success — issue the
|
|
154
|
+
* signed receipt + compliance-fields record and merge both into that same settlement header
|
|
155
|
+
* (`PAYMENT-RESPONSE`, or `X-PAYMENT-RESPONSE` for v1), where x402 wallets read them. The
|
|
156
|
+
* response body is left untouched unless `legacyBodyPlacement` is set.
|
|
157
|
+
*
|
|
158
|
+
* Once the settlement header reports success the buyer has paid, so a failure to issue does not
|
|
159
|
+
* throw: the response goes out exactly as the handler returned it, the error goes to `onError`,
|
|
160
|
+
* and with idempotency on the payment id is completed with that delivered response, so a retry
|
|
161
|
+
* replays it instead of finding the id stuck in flight. */
|
|
51
162
|
export function withAssure(handler, cfg) {
|
|
52
163
|
const now = cfg.clock ?? (() => Math.floor(Date.now() / 1000));
|
|
53
164
|
return async (req) => {
|
|
@@ -74,25 +185,48 @@ export function withAssure(handler, cfg) {
|
|
|
74
185
|
}
|
|
75
186
|
}
|
|
76
187
|
let res = await handler(req);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
188
|
+
if (cfg.advertise !== false)
|
|
189
|
+
res = advertiseCompliance(res, cfg.advertise ?? {});
|
|
190
|
+
const found = findSettlement(res.headers);
|
|
191
|
+
const settlement = found ? settlementInfo(found.response) : undefined;
|
|
192
|
+
if (found && settlement?.success) {
|
|
193
|
+
// Paid: nothing from here may cost the buyer the response. On any failure `res` stays the
|
|
194
|
+
// handler's own, and the idempotency entry below is completed with it.
|
|
195
|
+
try {
|
|
196
|
+
const url = new URL(req.url);
|
|
197
|
+
const overrides = cfg.toSettlementContext?.(req, settlement) ?? {};
|
|
198
|
+
const ctx = {
|
|
199
|
+
network: settlement.network ?? 'eip155:8453',
|
|
200
|
+
resourceUrl: url.origin + url.pathname,
|
|
201
|
+
payer: settlement.payer ?? 'unknown',
|
|
202
|
+
settledAt: now(),
|
|
203
|
+
supplyDescription: cfg.describeSupply?.(req) ?? url.pathname,
|
|
204
|
+
...(settlement.transaction !== undefined ? { txHash: settlement.transaction } : {}),
|
|
205
|
+
...overrides,
|
|
206
|
+
};
|
|
207
|
+
const issued = await cfg.assure.issueFor(ctx);
|
|
208
|
+
const headers = new Headers(res.headers);
|
|
209
|
+
headers.set(found.name, encodeX402Header(attachToSettlementResponse(found.response, issued)));
|
|
210
|
+
const body = cfg.legacyBodyPlacement && (res.headers.get('content-type') ?? '').includes('application/json')
|
|
211
|
+
? await jsonObjectBody(res)
|
|
212
|
+
: undefined;
|
|
213
|
+
if (body) {
|
|
214
|
+
headers.delete('content-length');
|
|
215
|
+
res = withHeaders(res, headers, JSON.stringify(attachToExtensions(body, issued)));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
res = withHeaders(res, headers);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
try {
|
|
223
|
+
// An async reporter's rejection is caught too: left unhandled it would end the process.
|
|
224
|
+
void Promise.resolve((cfg.onError ?? reportIssueError)(err)).catch(() => { });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// a throwing reporter must not cost the paid response either
|
|
228
|
+
}
|
|
229
|
+
}
|
|
96
230
|
}
|
|
97
231
|
if (onComplete) {
|
|
98
232
|
const body = await res.clone().text();
|
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
|
|
34
|
-
* (
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
/**
|
|
44
|
-
*
|
|
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
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
49
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
49
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
24
|
-
*
|
|
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';
|
package/dist/ledgerClient.d.ts
CHANGED
|
@@ -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
|
package/dist/ledgerClient.js
CHANGED
|
@@ -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',
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export declare function envDeps(env?: Record<string, string | undefined>): McpDe
|
|
|
8
8
|
* every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
|
|
9
9
|
export declare const MCP_SERVER_IDENTITY: {
|
|
10
10
|
readonly name: "tersign";
|
|
11
|
-
readonly version: "0.
|
|
11
|
+
readonly version: "0.6.0";
|
|
12
12
|
};
|
|
13
13
|
export declare function buildServer(deps: McpDeps): McpServer;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.
|
|
56
|
+
export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.6.0' };
|
|
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,
|
|
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: '
|
|
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
|
|
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
|
|
194
|
-
'Side effects: writes
|
|
195
|
-
'Returns the verdict, the rationale naming the rule applied, and the ledger signature over
|
|
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
|
},
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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).
|
|
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
|
package/dist/receipt/binding.js
CHANGED
|
@@ -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).
|
|
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
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
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
|
|
3
|
-
*
|
|
4
|
-
* the codec functions, so upstream churn lands here only. */
|
|
2
|
+
* specs/extensions/extension-offer-and-receipt.md, fetched 2026-07-07; receipt payload fields
|
|
3
|
+
* and response placement re-read 2026-09-29 at main 5eee1e3c35). Everything outside this module
|
|
4
|
+
* treats these as opaque via the codec functions, so upstream churn lands here only. */
|
|
5
5
|
export interface ReceiptPayload {
|
|
6
6
|
version: 1;
|
|
7
7
|
/** CAIP-2, e.g. "eip155:8453" */
|
|
@@ -36,9 +36,10 @@ export type SignedArtifact<P> = {
|
|
|
36
36
|
};
|
|
37
37
|
export type SignedReceipt = SignedArtifact<ReceiptPayload>;
|
|
38
38
|
export type SignedOffer = SignedArtifact<OfferPayload>;
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
39
|
+
/** Adjustment vocabulary as the proposed compliance-fields extension takes it from UCP: `type` is
|
|
40
|
+
* an OPEN string whose typical values are listed here, and only `status` is a fixed enum — so any
|
|
41
|
+
* other `type` string is accepted. */
|
|
42
|
+
export type AdjustmentType = 'refund' | 'return' | 'credit' | 'price_adjustment' | 'dispute' | 'cancellation' | (string & {});
|
|
42
43
|
export type AdjustmentStatus = 'pending' | 'completed' | 'failed';
|
|
43
44
|
export interface Adjustment {
|
|
44
45
|
type: AdjustmentType;
|
|
@@ -50,15 +51,24 @@ export interface Adjustment {
|
|
|
50
51
|
/** digest of the receipt/record this adjusts — the hash-chain link */
|
|
51
52
|
adjusts: `0x${string}`;
|
|
52
53
|
}
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
/** Compliance record v1 in the shape the `compliance-fields` extension proposes
|
|
55
|
+
* (x402-foundation/x402#2853, open — not settled spec) — a SEPARATE artifact bound to the base
|
|
56
|
+
* receipt by digest. The base receipt's EIP-712 schema is fixed upstream; extending it would
|
|
57
|
+
* break signatures, so compliance data composes by reference. The MINIMAL tier is isomorphic to
|
|
58
|
+
* the EU VAT Art 226b simplified-invoice content; whether a MINIMAL record suffices as an invoice
|
|
59
|
+
* is decided by the invoicing rules that apply to the supply (Art 219a), not by this record.
|
|
60
|
+
* FULL adds EN 16931-aligned fields. */
|
|
57
61
|
export interface ComplianceRecordV1 {
|
|
58
62
|
version: 1;
|
|
63
|
+
/** 1 = RFC 8785 (JCS) serialization, recordDigest = keccak256(utf8(canonical(record))).
|
|
64
|
+
* A MINIMAL member of the proposed extension; `buildMinimalRecord` sets it. Records built by
|
|
65
|
+
* tersign ≤0.5 omit it — readers accept both. */
|
|
66
|
+
canonicalizationVersion?: 1;
|
|
59
67
|
/** keccak256 of the canonicalized base receipt artifact */
|
|
60
68
|
receiptDigest: `0x${string}`;
|
|
61
|
-
/** sequential per issuer (Art 226(2))
|
|
69
|
+
/** sequential number per issuer series (Art 226(2)), assigned by the ISSUER before the record
|
|
70
|
+
* is attested. A sequence attested only by its issuer evidences ordering only — never that no
|
|
71
|
+
* record was omitted. `buildMinimalRecord` does not set it. */
|
|
62
72
|
seq?: number;
|
|
63
73
|
issuedAt: number;
|
|
64
74
|
issuer: {
|
|
@@ -109,7 +119,10 @@ export interface ComplianceRecordV1 {
|
|
|
109
119
|
/** hash-chain to the record this corrects/refunds (Art 226b(e); ViDA corrective-invoice ref) */
|
|
110
120
|
refundOf?: `0x${string}`;
|
|
111
121
|
adjustment?: Adjustment;
|
|
112
|
-
/** retention floor in years;
|
|
122
|
+
/** retention floor in years the issuer commits to; set it to at least the longest period
|
|
123
|
+
* that applies to the issuer (Art 247(1) leaves it to each Member State — e.g. DE § 14b UStG:
|
|
124
|
+
* eight years). 7 is the default where no longer period applies (HK IRO s.51C, MiCA 5+2), not
|
|
125
|
+
* a statement that 7 suffices. */
|
|
113
126
|
retentionYears: number;
|
|
114
127
|
}
|
|
115
128
|
/** EIP-712-signed attestation over a compliance record. The record itself is JSON (schema may
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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
|
|
3
|
-
*
|
|
4
|
-
* the codec functions, so upstream churn lands here only. */
|
|
2
|
+
* specs/extensions/extension-offer-and-receipt.md, fetched 2026-07-07; receipt payload fields
|
|
3
|
+
* and response placement re-read 2026-09-29 at main 5eee1e3c35). Everything outside this module
|
|
4
|
+
* treats these as opaque via the codec functions, so upstream churn lands here only. */
|
|
5
5
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tersign",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Tersign \u2014 the evidence layer for the agent economy. Counter-signed receipts, agent action records, idempotency enforcement, refunds, disputes, and jury-ready evidence envelopes for x402/agent-commerce sellers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|