tersign 0.4.11 → 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 +44 -20
- 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/canonical.d.ts +27 -0
- package/dist/canonical.js +46 -5
- package/dist/cli.js +7 -3
- package/dist/compliance/record.d.ts +18 -6
- package/dist/compliance/record.js +38 -14
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- 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 +28 -24
- package/dist/mcp/tools.d.ts +22 -4
- package/dist/mcp/tools.js +53 -5
- package/dist/receipt/binding.d.ts +75 -0
- package/dist/receipt/binding.js +161 -0
- package/dist/receipt/eip712.d.ts +23 -5
- package/dist/receipt/eip712.js +74 -9
- package/dist/receipt/known-keys.d.ts +23 -0
- package/dist/receipt/known-keys.js +59 -0
- 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/dist/verify-bin.js +212 -46
- package/dist/verify-report.d.ts +75 -0
- package/dist/verify-report.js +241 -0
- package/package.json +1 -1
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Account } from 'viem/accounts';
|
|
2
2
|
import type { Assure } from '../assure.js';
|
|
3
|
+
import { type SignerStatus } from '../receipt/binding.js';
|
|
3
4
|
import type { DisputeReason, EvidenceArtifactRef } from '../dispute/types.js';
|
|
4
5
|
import type { LedgerClient } from '../ledgerClient.js';
|
|
5
6
|
import type { ComplianceRecordV1, SignedComplianceRecord, SignedReceipt } from '../types.js';
|
|
@@ -31,10 +32,27 @@ export interface IssueReceiptArgs {
|
|
|
31
32
|
principal?: string | undefined;
|
|
32
33
|
}
|
|
33
34
|
export declare function issueReceiptTool(deps: McpDeps, args: IssueReceiptArgs): Promise<import("../assure.js").IssuedReceipt>;
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/** What the MCP verify tools return: the library result led by a one-sentence `verdict` and an
|
|
36
|
+
* explicit `signerStatus`, so an agent that reads only the first field is told what was NOT
|
|
37
|
+
* checked. Same shape as `python3 -m tersign verify`'s JSON (its verdict says PASS/FAIL where
|
|
38
|
+
* this says VALID/INVALID, the npm CLI's words). */
|
|
39
|
+
export interface VerifyToolResult {
|
|
40
|
+
verdict: string;
|
|
41
|
+
valid: boolean;
|
|
42
|
+
signer?: `0x${string}`;
|
|
43
|
+
signerStatus?: SignerStatus;
|
|
44
|
+
signerBound: boolean;
|
|
45
|
+
expectedSigner?: string;
|
|
46
|
+
testKey?: string;
|
|
47
|
+
digest?: `0x${string}`;
|
|
48
|
+
unsignedFields?: string[];
|
|
49
|
+
reason?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function verifyReceiptTool(artifact: SignedReceipt, expectedSigner?: string): Promise<VerifyToolResult>;
|
|
52
|
+
export declare function verifyRecordTool(record: ComplianceRecordV1, attestation: SignedComplianceRecord['attestation'], expectedSigner?: string): Promise<VerifyToolResult>;
|
|
36
53
|
export declare function recordRefundTool(deps: McpDeps, originalDigest: `0x${string}`, amount: string, reason: string): Promise<{
|
|
37
54
|
id: string;
|
|
55
|
+
status: string;
|
|
38
56
|
}>;
|
|
39
57
|
export interface OpenDisputeArgs {
|
|
40
58
|
receiptDigest: `0x${string}`;
|
|
@@ -51,8 +69,8 @@ export interface SubmitEvidenceArgs {
|
|
|
51
69
|
artifacts: EvidenceArtifactRef[];
|
|
52
70
|
}
|
|
53
71
|
export declare function submitEvidenceTool(deps: McpDeps, args: SubmitEvidenceArgs): Promise<unknown>;
|
|
54
|
-
/** Trigger deterministic adjudication (
|
|
55
|
-
* 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). */
|
|
56
74
|
export declare function adjudicateDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
|
|
57
75
|
export declare function getDisputeTool(deps: McpDeps, disputeDigest: `0x${string}`): Promise<unknown>;
|
|
58
76
|
export interface RecordDisclosureArgs {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { verifyReceipt } from '../receipt/eip712.js';
|
|
2
|
-
import { verifyComplianceRecord } from '../compliance/record.js';
|
|
2
|
+
import { recordDigest, verifyComplianceRecord } from '../compliance/record.js';
|
|
3
|
+
import { digestOf } from '../canonical.js';
|
|
4
|
+
import { findLoneSurrogate, signerStatus } from '../receipt/binding.js';
|
|
5
|
+
import { verdictSentence } from '../verify-report.js';
|
|
3
6
|
import { signDispute, signEvidence } from '../dispute/sign.js';
|
|
4
7
|
async function ledgerFetch(base, path, init) {
|
|
5
8
|
const url = `${base.replace(/\/$/, '')}${path}`;
|
|
@@ -30,11 +33,56 @@ export async function issueReceiptTool(deps, args) {
|
|
|
30
33
|
};
|
|
31
34
|
return deps.assure.issueFor(ctx);
|
|
32
35
|
}
|
|
36
|
+
function report(r, expectedSigner, digest, what) {
|
|
37
|
+
const status = signerStatus(r, expectedSigner);
|
|
38
|
+
// Key order is the Python CLI's: verdict first, the signer and its status next to each other.
|
|
39
|
+
return {
|
|
40
|
+
verdict: verdictSentence(r, expectedSigner, 'expectedSigner', what),
|
|
41
|
+
valid: r.valid,
|
|
42
|
+
...(r.signer ? { signer: r.signer } : {}),
|
|
43
|
+
...(status ? { signerStatus: status } : {}),
|
|
44
|
+
signerBound: r.signerBound,
|
|
45
|
+
...(expectedSigner ? { expectedSigner } : {}),
|
|
46
|
+
...(r.testKey ? { testKey: r.testKey } : {}),
|
|
47
|
+
...(digest ? { digest } : {}),
|
|
48
|
+
...(r.unsignedFields?.length ? { unsignedFields: r.unsignedFields } : {}),
|
|
49
|
+
...(r.reason ? { reason: r.reason } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** A digest failure (a float, an out-of-range integer) is a FAIL: an artifact with no canonical
|
|
53
|
+
* content address cannot be the one a ledger recorded. The Python twin decides it the same way. */
|
|
54
|
+
function tryDigest(f) {
|
|
55
|
+
try {
|
|
56
|
+
return { digest: f() };
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** A lone UTF-16 surrogate anywhere in what is verified: UTF-8 cannot carry it, so the signed and
|
|
63
|
+
* digested text is U+FFFD, not what the object says. Package text only — no path, since key names
|
|
64
|
+
* are chosen by whoever wrote the object. */
|
|
65
|
+
const LONE_SURROGATE_REASON = 'a string in the object holds a lone UTF-16 surrogate (a \\uD800-\\uDFFF escape) that UTF-8 cannot carry: the signed and digested text would be U+FFFD, not this text';
|
|
33
66
|
export async function verifyReceiptTool(artifact, expectedSigner) {
|
|
34
|
-
|
|
67
|
+
if (findLoneSurrogate(artifact) !== null) {
|
|
68
|
+
return report({ valid: false, signerBound: false, reason: LONE_SURROGATE_REASON }, expectedSigner, undefined, 'receipt');
|
|
69
|
+
}
|
|
70
|
+
const r = await verifyReceipt(artifact, expectedSigner);
|
|
71
|
+
if (!r.signer)
|
|
72
|
+
return report(r, expectedSigner, undefined, 'receipt');
|
|
73
|
+
const d = tryDigest(() => digestOf(artifact));
|
|
74
|
+
if (d.error !== undefined) {
|
|
75
|
+
return report({ valid: false, signerBound: false, reason: `cannot compute the canonical digest: ${d.error}` }, expectedSigner, undefined, 'receipt');
|
|
76
|
+
}
|
|
77
|
+
return report(r, expectedSigner, d.digest, 'receipt');
|
|
35
78
|
}
|
|
36
79
|
export async function verifyRecordTool(record, attestation, expectedSigner) {
|
|
37
|
-
|
|
80
|
+
if (findLoneSurrogate({ record, attestation }) !== null) {
|
|
81
|
+
return report({ valid: false, signerBound: false, reason: LONE_SURROGATE_REASON }, expectedSigner, undefined, 'compliance record');
|
|
82
|
+
}
|
|
83
|
+
const r = await verifyComplianceRecord({ record, attestation }, expectedSigner);
|
|
84
|
+
const d = tryDigest(() => recordDigest(record));
|
|
85
|
+
return report(r, expectedSigner, d.digest, 'compliance record');
|
|
38
86
|
}
|
|
39
87
|
export async function recordRefundTool(deps, originalDigest, amount, reason) {
|
|
40
88
|
if (!deps.ledger)
|
|
@@ -71,8 +119,8 @@ export async function submitEvidenceTool(deps, args) {
|
|
|
71
119
|
...(args.role === 'respondent' && deps.ledgerHttp.apiKey !== undefined ? { apiKey: deps.ledgerHttp.apiKey } : {}),
|
|
72
120
|
});
|
|
73
121
|
}
|
|
74
|
-
/** Trigger deterministic adjudication (
|
|
75
|
-
* 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). */
|
|
76
124
|
export async function adjudicateDisputeTool(deps, disputeDigest) {
|
|
77
125
|
if (!deps.ledgerHttp)
|
|
78
126
|
throw new Error('ledger URL not configured — set TERSIGN_LEDGER_URL');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const ADDRESS_RE: RegExp;
|
|
2
|
+
export declare const MALFORMED_EXPECTED_SIGNER = "expectedSigner is not a 20-byte 0x-prefixed hex address";
|
|
3
|
+
/** undefined, null and '' mean "not supplied". Anything else must be a 20-byte hex address:
|
|
4
|
+
* a malformed value is a FAILURE, never a silently skipped comparison (0, false, [] and {}
|
|
5
|
+
* included — a caller who passed something meant to bind). */
|
|
6
|
+
export declare function parseExpectedSigner(v: unknown): {
|
|
7
|
+
ok: true;
|
|
8
|
+
value: string | undefined;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export type SignerStatus = 'BOUND' | 'UNAUTHENTICATED' | 'MISMATCH';
|
|
14
|
+
/** The fields every binding-aware verify result carries. */
|
|
15
|
+
export interface SignerBinding {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
signer?: `0x${string}`;
|
|
18
|
+
/** true only when an expected signer was supplied AND the signature recovers to exactly it.
|
|
19
|
+
* false with valid:true means `signer` is whatever the artifact's own signature recovers to:
|
|
20
|
+
* that proves neither who signed nor that the artifact is unmodified. */
|
|
21
|
+
signerBound: boolean;
|
|
22
|
+
/** present when the recovered signer is a PUBLISHED test key (see known-keys.ts): anyone can
|
|
23
|
+
* produce that signature, so even a bound match says nothing about who issued it. */
|
|
24
|
+
testKey?: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
/** BOUND: matched the expected signer. MISMATCH: an expected signer was supplied and the
|
|
28
|
+
* signature recovers to a different address. UNAUTHENTICATED: none was supplied. Undefined when
|
|
29
|
+
* nothing was recovered. Decided from the inputs, never from the reason text. */
|
|
30
|
+
export declare function signerStatus(r: SignerBinding, expected: string | undefined): SignerStatus | undefined;
|
|
31
|
+
/** Attach signerBound + testKey to a recovered signer, comparing against `expected` when given. */
|
|
32
|
+
export declare function bindSigner(signer: `0x${string}`, expected: string | undefined, mismatchReason: string): SignerBinding;
|
|
33
|
+
/** Why an EIP-712 `signature` is not THE canonical encoding, or undefined. Checked BEFORE
|
|
34
|
+
* recovery, with the same rules as the Python twin's verify_receipt (sdk-py tersign/verify.py
|
|
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:
|
|
39
|
+
*
|
|
40
|
+
* "0x" + 130 LOWER-CASE hex digits = r (32 bytes) || s (32 bytes) || v (1 byte), with
|
|
41
|
+
* 1 <= r, s < n; s <= n/2 (low-s); v = 27 or 28 (0x1b / 0x1c) — what viem's
|
|
42
|
+
* serializeSignature emits, so every signature this package and the ledger produce.
|
|
43
|
+
*
|
|
44
|
+
* Everything else is refused, each with its own reason: a non-string ({r,s,v} object, 65-number
|
|
45
|
+
* array), a missing 0x, any other length, an upper-case hex digit, the recovery-id twin (v = 0/1
|
|
46
|
+
* for 27/28), an out-of-range r/s, and the high-s twin. viem accepts every one of those and
|
|
47
|
+
* recovers the real signer from each, and each re-encoding gives the file a different content
|
|
48
|
+
* digest — so without this one issuance had several "also valid" byte-distinct copies, each
|
|
49
|
+
* printing a bound VALID for signature bytes the issuer never produced (high-s, object and array
|
|
50
|
+
* found 2026-09-27; v 0/1 and upper-case hex found by the release review the same day).
|
|
51
|
+
*
|
|
52
|
+
* Scope: verifyReceipt, verifyComplianceRecord and the CLI and MCP tools built on them. The
|
|
53
|
+
* action-record, dispute and evidence verifiers (evidence/action.ts, dispute/sign.ts) do NOT call
|
|
54
|
+
* this and still accept what viem accepts (PUNT-REGISTER R8). Neither does the ledger's ingest
|
|
55
|
+
* (SECURITY-AUDIT A8), so a receipt the ledger counter-signed with a non-canonical signature is
|
|
56
|
+
* refused here. The reasons are package text only: nothing from the file is echoed. */
|
|
57
|
+
export declare function signatureError(sig: unknown): string | undefined;
|
|
58
|
+
/** A UTF-16 surrogate with no partner. JSON's \uD800-\uDFFF escapes can put one in a parsed string;
|
|
59
|
+
* UTF-8 cannot carry it, so the EIP-712 encoder (viem's TextEncoder) signs U+FFFD in its place and
|
|
60
|
+
* the text the file holds is not the text that was signed. The Python verifier refuses such a file
|
|
61
|
+
* outright ('utf-8' codec can't encode). */
|
|
62
|
+
export declare const LONE_SURROGATE: RegExp;
|
|
63
|
+
/** The first string (key or value) anywhere in `v` holding a lone surrogate, as a path, or null. */
|
|
64
|
+
export declare function findLoneSurrogate(v: unknown, path?: string): string | null;
|
|
65
|
+
export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
|
|
66
|
+
/** Code-point order — the order Python's sorted() gives, so both verifiers list fields alike. */
|
|
67
|
+
export declare function byCodePoint(a: string, b: string): number;
|
|
68
|
+
/** Render text that came from the artifact (a field NAME, a ledger's string) so it can never
|
|
69
|
+
* forge output: control, format and line/paragraph-separator characters become \u escapes.
|
|
70
|
+
* Without this a payload key such as "x\nVALID" would print a bare VALID line of its own. */
|
|
71
|
+
export declare function safeText(s: string, max?: number): string;
|
|
72
|
+
/** A field name as a JSON string literal — `"payload.amount"` — clipped, quotes and
|
|
73
|
+
* backslashes escaped by JSON.stringify, and the invisible characters it leaves raw
|
|
74
|
+
* (U+2028/U+2029, bidi and other format controls) escaped too. */
|
|
75
|
+
export declare function quoteField(name: string): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** What an EIP-712 verify can and cannot establish about WHO signed — shared by verifyReceipt,
|
|
2
|
+
* verifyComplianceRecord, the `tersign verify` CLI and the MCP verify tools, so those four
|
|
3
|
+
* surfaces cannot drift apart on the one question a verify result is read for. verifyActionRecord,
|
|
4
|
+
* verifyDispute and verifyEvidence do NOT use it: they have no signer binding and no canonical
|
|
5
|
+
* signature check (PUNT-REGISTER R8).
|
|
6
|
+
*
|
|
7
|
+
* ECDSA recovery yields an address for ANY payload and ANY well-formed signature. An edited
|
|
8
|
+
* receipt therefore still "verifies" — it recovers a different address. A recovered signer is
|
|
9
|
+
* evidence of authorship only when it is compared against an address the caller obtained
|
|
10
|
+
* somewhere else; without that comparison it is UNAUTHENTICATED, and every result says so. */
|
|
11
|
+
import { publishedKeyLabel } from './known-keys.js';
|
|
12
|
+
export const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
13
|
+
export const MALFORMED_EXPECTED_SIGNER = 'expectedSigner is not a 20-byte 0x-prefixed hex address';
|
|
14
|
+
/** undefined, null and '' mean "not supplied". Anything else must be a 20-byte hex address:
|
|
15
|
+
* a malformed value is a FAILURE, never a silently skipped comparison (0, false, [] and {}
|
|
16
|
+
* included — a caller who passed something meant to bind). */
|
|
17
|
+
export function parseExpectedSigner(v) {
|
|
18
|
+
if (v === undefined || v === null || v === '')
|
|
19
|
+
return { ok: true, value: undefined };
|
|
20
|
+
if (typeof v !== 'string' || !ADDRESS_RE.test(v))
|
|
21
|
+
return { ok: false, reason: MALFORMED_EXPECTED_SIGNER };
|
|
22
|
+
return { ok: true, value: v };
|
|
23
|
+
}
|
|
24
|
+
/** BOUND: matched the expected signer. MISMATCH: an expected signer was supplied and the
|
|
25
|
+
* signature recovers to a different address. UNAUTHENTICATED: none was supplied. Undefined when
|
|
26
|
+
* nothing was recovered. Decided from the inputs, never from the reason text. */
|
|
27
|
+
export function signerStatus(r, expected) {
|
|
28
|
+
if (!r.signer)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (r.signerBound)
|
|
31
|
+
return 'BOUND';
|
|
32
|
+
return expected ? 'MISMATCH' : 'UNAUTHENTICATED';
|
|
33
|
+
}
|
|
34
|
+
/** Attach signerBound + testKey to a recovered signer, comparing against `expected` when given. */
|
|
35
|
+
export function bindSigner(signer, expected, mismatchReason) {
|
|
36
|
+
const out = { valid: true, signer, signerBound: expected !== undefined };
|
|
37
|
+
const label = publishedKeyLabel(signer);
|
|
38
|
+
if (label)
|
|
39
|
+
out.testKey = label;
|
|
40
|
+
if (expected !== undefined && signer.toLowerCase() !== expected.toLowerCase()) {
|
|
41
|
+
out.valid = false;
|
|
42
|
+
out.signerBound = false;
|
|
43
|
+
out.reason = mismatchReason;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** secp256k1 group order. A signature's s must be at most N/2 (Ethereum's canonical low-s form). */
|
|
48
|
+
const SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
49
|
+
/** Why an EIP-712 `signature` is not THE canonical encoding, or undefined. Checked BEFORE
|
|
50
|
+
* recovery, with the same rules as the Python twin's verify_receipt (sdk-py tersign/verify.py
|
|
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:
|
|
55
|
+
*
|
|
56
|
+
* "0x" + 130 LOWER-CASE hex digits = r (32 bytes) || s (32 bytes) || v (1 byte), with
|
|
57
|
+
* 1 <= r, s < n; s <= n/2 (low-s); v = 27 or 28 (0x1b / 0x1c) — what viem's
|
|
58
|
+
* serializeSignature emits, so every signature this package and the ledger produce.
|
|
59
|
+
*
|
|
60
|
+
* Everything else is refused, each with its own reason: a non-string ({r,s,v} object, 65-number
|
|
61
|
+
* array), a missing 0x, any other length, an upper-case hex digit, the recovery-id twin (v = 0/1
|
|
62
|
+
* for 27/28), an out-of-range r/s, and the high-s twin. viem accepts every one of those and
|
|
63
|
+
* recovers the real signer from each, and each re-encoding gives the file a different content
|
|
64
|
+
* digest — so without this one issuance had several "also valid" byte-distinct copies, each
|
|
65
|
+
* printing a bound VALID for signature bytes the issuer never produced (high-s, object and array
|
|
66
|
+
* found 2026-09-27; v 0/1 and upper-case hex found by the release review the same day).
|
|
67
|
+
*
|
|
68
|
+
* Scope: verifyReceipt, verifyComplianceRecord and the CLI and MCP tools built on them. The
|
|
69
|
+
* action-record, dispute and evidence verifiers (evidence/action.ts, dispute/sign.ts) do NOT call
|
|
70
|
+
* this and still accept what viem accepts (PUNT-REGISTER R8). Neither does the ledger's ingest
|
|
71
|
+
* (SECURITY-AUDIT A8), so a receipt the ledger counter-signed with a non-canonical signature is
|
|
72
|
+
* refused here. The reasons are package text only: nothing from the file is echoed. */
|
|
73
|
+
export function signatureError(sig) {
|
|
74
|
+
if (typeof sig !== 'string') {
|
|
75
|
+
const t = sig === null ? 'null' : Array.isArray(sig) ? 'an array' : typeof sig === 'undefined' ? 'missing' : typeof sig === 'object' ? 'an object' : `a ${typeof sig}`;
|
|
76
|
+
return `signature must be a 0x-prefixed hex string of 65 bytes (r||s||v), not ${t}`;
|
|
77
|
+
}
|
|
78
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(sig))
|
|
79
|
+
return 'signature must be a 0x-prefixed hex string of 65 bytes (r||s||v)';
|
|
80
|
+
if (/[A-F]/.test(sig))
|
|
81
|
+
return 'signature hex must be lower-case (non-canonical)';
|
|
82
|
+
const r = BigInt(`0x${sig.slice(2, 66)}`);
|
|
83
|
+
const s = BigInt(`0x${sig.slice(66, 130)}`);
|
|
84
|
+
const v = Number.parseInt(sig.slice(130, 132), 16);
|
|
85
|
+
if (v === 0 || v === 1)
|
|
86
|
+
return `recovery id ${v} rejected (non-canonical): v must be 27 or 28`;
|
|
87
|
+
if (v !== 27 && v !== 28)
|
|
88
|
+
return `unsupported recovery id ${v}`;
|
|
89
|
+
if (r < 1n || r >= SECP256K1_N || s < 1n || s >= SECP256K1_N)
|
|
90
|
+
return 'r/s out of range';
|
|
91
|
+
if (s > SECP256K1_N / 2n)
|
|
92
|
+
return 'high-s signature rejected (non-canonical)';
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/** A UTF-16 surrogate with no partner. JSON's \uD800-\uDFFF escapes can put one in a parsed string;
|
|
96
|
+
* UTF-8 cannot carry it, so the EIP-712 encoder (viem's TextEncoder) signs U+FFFD in its place and
|
|
97
|
+
* the text the file holds is not the text that was signed. The Python verifier refuses such a file
|
|
98
|
+
* outright ('utf-8' codec can't encode). */
|
|
99
|
+
export const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
100
|
+
/** The first string (key or value) anywhere in `v` holding a lone surrogate, as a path, or null. */
|
|
101
|
+
export function findLoneSurrogate(v, path = '$') {
|
|
102
|
+
if (typeof v === 'string')
|
|
103
|
+
return LONE_SURROGATE.test(v) ? path : null;
|
|
104
|
+
if (Array.isArray(v)) {
|
|
105
|
+
for (let i = 0; i < v.length; i++) {
|
|
106
|
+
const hit = findLoneSurrogate(v[i], `${path}[${i}]`);
|
|
107
|
+
if (hit)
|
|
108
|
+
return hit;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (isPlainObject(v)) {
|
|
113
|
+
for (const [k, x] of Object.entries(v)) {
|
|
114
|
+
if (LONE_SURROGATE.test(k))
|
|
115
|
+
return `${path} (a key)`;
|
|
116
|
+
const hit = findLoneSurrogate(x, `${path}.${k}`);
|
|
117
|
+
if (hit)
|
|
118
|
+
return hit;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
export function isPlainObject(v) {
|
|
124
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
125
|
+
}
|
|
126
|
+
/** Code-point order — the order Python's sorted() gives, so both verifiers list fields alike. */
|
|
127
|
+
export function byCodePoint(a, b) {
|
|
128
|
+
const x = a[Symbol.iterator]();
|
|
129
|
+
const y = b[Symbol.iterator]();
|
|
130
|
+
for (;;) {
|
|
131
|
+
const p = x.next();
|
|
132
|
+
const q = y.next();
|
|
133
|
+
if (p.done || q.done)
|
|
134
|
+
return (p.done ? 0 : 1) - (q.done ? 0 : 1);
|
|
135
|
+
const d = (p.value.codePointAt(0) ?? 0) - (q.value.codePointAt(0) ?? 0);
|
|
136
|
+
if (d !== 0)
|
|
137
|
+
return d;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Render text that came from the artifact (a field NAME, a ledger's string) so it can never
|
|
141
|
+
* forge output: control, format and line/paragraph-separator characters become \u escapes.
|
|
142
|
+
* Without this a payload key such as "x\nVALID" would print a bare VALID line of its own. */
|
|
143
|
+
export function safeText(s, max = 96) {
|
|
144
|
+
return escapeInvisible(clip(s, max));
|
|
145
|
+
}
|
|
146
|
+
/** A field name as a JSON string literal — `"payload.amount"` — clipped, quotes and
|
|
147
|
+
* backslashes escaped by JSON.stringify, and the invisible characters it leaves raw
|
|
148
|
+
* (U+2028/U+2029, bidi and other format controls) escaped too. */
|
|
149
|
+
export function quoteField(name) {
|
|
150
|
+
return escapeInvisible(JSON.stringify(clip(name, 64)));
|
|
151
|
+
}
|
|
152
|
+
function clip(s, max) {
|
|
153
|
+
const cps = [...s];
|
|
154
|
+
return cps.length > max ? cps.slice(0, max).join('') + '…' : s;
|
|
155
|
+
}
|
|
156
|
+
function escapeInvisible(s) {
|
|
157
|
+
return s.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (ch) => {
|
|
158
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
159
|
+
return cp > 0xffff ? `\\u{${cp.toString(16)}}` : `\\u${cp.toString(16).padStart(4, '0')}`;
|
|
160
|
+
});
|
|
161
|
+
}
|
package/dist/receipt/eip712.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Account } from 'viem/accounts';
|
|
2
2
|
import type { OfferPayload, ReceiptPayload, SignedOffer, SignedReceipt } from '../types.js';
|
|
3
|
+
import { type SignerBinding } from './binding.js';
|
|
3
4
|
/** Canonical EIP-712 material from the merged offer-receipt extension. Domain chainId is
|
|
4
5
|
* hardcoded to 1 by spec (off-chain signing format; payment network lives in payload.network). */
|
|
5
6
|
export declare const RECEIPT_DOMAIN: {
|
|
@@ -62,11 +63,28 @@ export declare const OFFER_TYPES: {
|
|
|
62
63
|
};
|
|
63
64
|
export declare function signReceipt(payload: ReceiptPayload, account: Account): Promise<SignedReceipt>;
|
|
64
65
|
export declare function signOffer(payload: OfferPayload, account: Account, acceptIndex?: number): Promise<SignedOffer>;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/** What the EIP-712 Receipt signature covers. Anything else in the artifact rides along
|
|
67
|
+
* unsigned: it changes the canonical digest (the ledger's content address) but not the
|
|
68
|
+
* recovered signer. */
|
|
69
|
+
export declare const SIGNED_RECEIPT_FIELDS: ("version" | "network" | "resourceUrl" | "payer" | "issuedAt" | "transaction")[];
|
|
70
|
+
/** Fields present in `artifact` that the signature does not cover, as paths relative to the
|
|
71
|
+
* artifact — `payload.<k>` first, then top-level keys — in code-point order. */
|
|
72
|
+
export declare function unsignedReceiptFields(artifact: Record<string, unknown>): string[];
|
|
73
|
+
/** valid the signature is well-formed and recovers to an address — and, when
|
|
74
|
+
* expectedSigner is given, to exactly that address.
|
|
75
|
+
* signerBound true only when expectedSigner was supplied and matched. When false, `signer`
|
|
76
|
+
* is whatever the receipt's own signature recovers to, and recovery yields an
|
|
77
|
+
* address for ANY payload: an edited receipt still returns valid:true, with a
|
|
78
|
+
* different signer. valid:true with signerBound:false proves neither who signed
|
|
79
|
+
* nor that the payload is unmodified.
|
|
80
|
+
* testKey the recovered signer is a PUBLISHED test key (known-keys.ts).
|
|
81
|
+
* unsignedFields fields present in the artifact that the signature does not cover.
|
|
82
|
+
* The same result shape as the Python twin's verify_receipt (minus its digest, which the
|
|
83
|
+
* callers here compute themselves). */
|
|
84
|
+
export interface VerifyResult extends SignerBinding {
|
|
85
|
+
unsignedFields?: string[];
|
|
69
86
|
}
|
|
70
87
|
/** Verify an EIP-712 receipt. `expectedSigner` implements the spec's payTo-key authorization
|
|
71
|
-
* model; pass the seller's payTo address (or a registry-resolved key)
|
|
88
|
+
* model; pass the seller's payTo address (or a registry-resolved key), obtained out-of-band,
|
|
89
|
+
* to enforce it. Without it the recovered signer is UNAUTHENTICATED (signerBound:false). */
|
|
72
90
|
export declare function verifyReceipt(artifact: SignedReceipt, expectedSigner?: string): Promise<VerifyResult>;
|
package/dist/receipt/eip712.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recoverTypedDataAddress } from 'viem';
|
|
2
|
+
import { LONE_SURROGATE, bindSigner, byCodePoint, isPlainObject, parseExpectedSigner, signatureError } from './binding.js';
|
|
2
3
|
/** Canonical EIP-712 material from the merged offer-receipt extension. Domain chainId is
|
|
3
4
|
* hardcoded to 1 by spec (off-chain signing format; payment network lives in payload.network). */
|
|
4
5
|
export const RECEIPT_DOMAIN = { name: 'x402 receipt', version: '1', chainId: 1n };
|
|
@@ -71,25 +72,89 @@ export async function signOffer(payload, account, acceptIndex) {
|
|
|
71
72
|
? { format: 'eip712', payload, signature }
|
|
72
73
|
: { format: 'eip712', payload, signature, acceptIndex };
|
|
73
74
|
}
|
|
75
|
+
/** What the EIP-712 Receipt signature covers. Anything else in the artifact rides along
|
|
76
|
+
* unsigned: it changes the canonical digest (the ledger's content address) but not the
|
|
77
|
+
* recovered signer. */
|
|
78
|
+
export const SIGNED_RECEIPT_FIELDS = RECEIPT_TYPES.Receipt.map((f) => f.name);
|
|
79
|
+
const ENVELOPE_FIELDS = new Set(['format', 'payload', 'signature']);
|
|
80
|
+
const SIGNED = new Set(SIGNED_RECEIPT_FIELDS);
|
|
81
|
+
/** Fields present in `artifact` that the signature does not cover, as paths relative to the
|
|
82
|
+
* artifact — `payload.<k>` first, then top-level keys — in code-point order. */
|
|
83
|
+
export function unsignedReceiptFields(artifact) {
|
|
84
|
+
const payload = isPlainObject(artifact.payload) ? artifact.payload : {};
|
|
85
|
+
return [
|
|
86
|
+
...Object.keys(payload).filter((k) => !SIGNED.has(k)).sort(byCodePoint).map((k) => `payload.${k}`),
|
|
87
|
+
...Object.keys(artifact).filter((k) => !ENVELOPE_FIELDS.has(k)).sort(byCodePoint),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function typeName(v) {
|
|
91
|
+
if (v === null)
|
|
92
|
+
return 'null';
|
|
93
|
+
if (Array.isArray(v))
|
|
94
|
+
return 'array';
|
|
95
|
+
if (typeof v === 'number' && !Number.isInteger(v))
|
|
96
|
+
return 'a non-integer number';
|
|
97
|
+
if (typeof v === 'number')
|
|
98
|
+
return v < 0 ? 'a negative integer' : 'an integer outside the safe range';
|
|
99
|
+
return typeof v;
|
|
100
|
+
}
|
|
101
|
+
/** The payload's signed fields, checked by JSON type before anything is recovered. BigInt()
|
|
102
|
+
* coerces true, "1", " 1783761710 " and "0x6a4…" to the signed number: each such edit recovered
|
|
103
|
+
* the real signer under a DIFFERENT digest, and nothing reported it. The ledger counter-signs
|
|
104
|
+
* only JSON safe integers here, and the Python verifier applies the same rule. */
|
|
105
|
+
function signedFieldError(p) {
|
|
106
|
+
for (const k of ['version', 'issuedAt']) {
|
|
107
|
+
const v = p[k];
|
|
108
|
+
const ok = (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0) || (typeof v === 'bigint' && v >= 0n && v < 2n ** 256n);
|
|
109
|
+
if (!ok)
|
|
110
|
+
return `payload.${k} must be a JSON integer, not ${typeName(v)}: the signature covers the number, so another spelling of it is unsigned text`;
|
|
111
|
+
}
|
|
112
|
+
for (const k of ['network', 'resourceUrl', 'payer', 'transaction']) {
|
|
113
|
+
if (typeof p[k] !== 'string')
|
|
114
|
+
return `payload.${k} must be a string, not ${typeName(p[k])}`;
|
|
115
|
+
if (LONE_SURROGATE.test(p[k])) {
|
|
116
|
+
return `payload.${k} holds a lone UTF-16 surrogate (a \\uD800-\\uDFFF escape) that UTF-8 cannot carry: the signature covers U+FFFD in its place, not this text`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
74
121
|
/** Verify an EIP-712 receipt. `expectedSigner` implements the spec's payTo-key authorization
|
|
75
|
-
* model; pass the seller's payTo address (or a registry-resolved key)
|
|
122
|
+
* model; pass the seller's payTo address (or a registry-resolved key), obtained out-of-band,
|
|
123
|
+
* to enforce it. Without it the recovered signer is UNAUTHENTICATED (signerBound:false). */
|
|
76
124
|
export async function verifyReceipt(artifact, expectedSigner) {
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
const expected = parseExpectedSigner(expectedSigner);
|
|
126
|
+
if (!expected.ok)
|
|
127
|
+
return { valid: false, signerBound: false, reason: expected.reason };
|
|
128
|
+
const notReceipt = { valid: false, signerBound: false, reason: 'not a signed receipt: expected {format, payload{...}, signature}' };
|
|
129
|
+
if (!isPlainObject(artifact))
|
|
130
|
+
return notReceipt;
|
|
131
|
+
if (artifact.format !== 'eip712') {
|
|
132
|
+
return { valid: false, signerBound: false, reason: 'jws verification not implemented in v0' };
|
|
133
|
+
}
|
|
134
|
+
if (!isPlainObject(artifact.payload))
|
|
135
|
+
return notReceipt;
|
|
136
|
+
const badField = signedFieldError(artifact.payload);
|
|
137
|
+
if (badField)
|
|
138
|
+
return { valid: false, signerBound: false, reason: badField };
|
|
139
|
+
const badSig = signatureError(artifact.signature);
|
|
140
|
+
if (badSig)
|
|
141
|
+
return { valid: false, signerBound: false, reason: badSig };
|
|
142
|
+
let signer;
|
|
79
143
|
try {
|
|
80
|
-
|
|
144
|
+
signer = await recoverTypedDataAddress({
|
|
81
145
|
domain: RECEIPT_DOMAIN,
|
|
82
146
|
types: RECEIPT_TYPES,
|
|
83
147
|
primaryType: 'Receipt',
|
|
84
148
|
message: receiptMessage(artifact.payload),
|
|
85
149
|
signature: artifact.signature,
|
|
86
150
|
});
|
|
87
|
-
if (expectedSigner && signer.toLowerCase() !== expectedSigner.toLowerCase()) {
|
|
88
|
-
return { valid: false, signer, reason: 'signer does not match expected authorization key' };
|
|
89
|
-
}
|
|
90
|
-
return { valid: true, signer };
|
|
91
151
|
}
|
|
92
152
|
catch (e) {
|
|
93
|
-
return { valid: false, reason: e instanceof Error ? e.message : 'signature recovery failed' };
|
|
153
|
+
return { valid: false, signerBound: false, reason: e instanceof Error ? e.message : 'signature recovery failed' };
|
|
94
154
|
}
|
|
155
|
+
const out = bindSigner(signer, expected.value, 'signer does not match expected authorization key');
|
|
156
|
+
const unsigned = unsignedReceiptFields(artifact);
|
|
157
|
+
if (unsigned.length)
|
|
158
|
+
out.unsignedFields = unsigned;
|
|
159
|
+
return out;
|
|
95
160
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Signer addresses whose private keys are PUBLISHED — anyone can sign as them.
|
|
2
|
+
*
|
|
3
|
+
* A signature from one of these keys recovers and verifies like any other, so a verifier that
|
|
4
|
+
* stays silent about it hands a stranger a receipt that looks authored when anyone could have
|
|
5
|
+
* minted it. They are what worked examples and test suites sign with (ours included: the
|
|
6
|
+
* synthetic evidence bundle uses dev-mnemonic accounts #0 and #1), which is exactly why a
|
|
7
|
+
* real-looking artifact carrying one must be flagged rather than passed quietly.
|
|
8
|
+
*
|
|
9
|
+
* Twin of `sdk-py/tersign/known_keys.py`. Neither copy is transcribed from the other: each is
|
|
10
|
+
* DERIVED from the published secrets and pinned by its own suite — test/known-keys.test.ts
|
|
11
|
+
* re-derives every address here with viem, the Python suite re-derives its table with its own
|
|
12
|
+
* secp256k1 — and the same test compares the two tables entry by entry (address AND label)
|
|
13
|
+
* wherever the monorepo carries both. A key added to one side without the other fails there.
|
|
14
|
+
* Deliberately narrow: keys whose secret is published as a convention, not every key that has
|
|
15
|
+
* ever leaked. */
|
|
16
|
+
/** BIP-39 mnemonic shipped as the default dev account set by Hardhat (20 accounts) and Anvil
|
|
17
|
+
* (10), derived at m/44'/60'/0'/0/i. */
|
|
18
|
+
export declare const DEV_MNEMONIC = "test test test test test test test test test test test junk";
|
|
19
|
+
/** lowercase address → the published secret it comes from. A Map, not an object literal: a
|
|
20
|
+
* lookup of 'constructor' or '__proto__' on a plain object answers with a prototype member. */
|
|
21
|
+
export declare const PUBLISHED_TEST_KEYS: ReadonlyMap<string, string>;
|
|
22
|
+
/** The published-secret label for `address`, or undefined when it is not a known test key. */
|
|
23
|
+
export declare function publishedKeyLabel(address: unknown): string | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Signer addresses whose private keys are PUBLISHED — anyone can sign as them.
|
|
2
|
+
*
|
|
3
|
+
* A signature from one of these keys recovers and verifies like any other, so a verifier that
|
|
4
|
+
* stays silent about it hands a stranger a receipt that looks authored when anyone could have
|
|
5
|
+
* minted it. They are what worked examples and test suites sign with (ours included: the
|
|
6
|
+
* synthetic evidence bundle uses dev-mnemonic accounts #0 and #1), which is exactly why a
|
|
7
|
+
* real-looking artifact carrying one must be flagged rather than passed quietly.
|
|
8
|
+
*
|
|
9
|
+
* Twin of `sdk-py/tersign/known_keys.py`. Neither copy is transcribed from the other: each is
|
|
10
|
+
* DERIVED from the published secrets and pinned by its own suite — test/known-keys.test.ts
|
|
11
|
+
* re-derives every address here with viem, the Python suite re-derives its table with its own
|
|
12
|
+
* secp256k1 — and the same test compares the two tables entry by entry (address AND label)
|
|
13
|
+
* wherever the monorepo carries both. A key added to one side without the other fails there.
|
|
14
|
+
* Deliberately narrow: keys whose secret is published as a convention, not every key that has
|
|
15
|
+
* ever leaked. */
|
|
16
|
+
/** BIP-39 mnemonic shipped as the default dev account set by Hardhat (20 accounts) and Anvil
|
|
17
|
+
* (10), derived at m/44'/60'/0'/0/i. */
|
|
18
|
+
export const DEV_MNEMONIC = 'test test test test test test test test test test test junk';
|
|
19
|
+
const DEV_MNEMONIC_ADDRESSES = [
|
|
20
|
+
'0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
|
|
21
|
+
'0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
|
|
22
|
+
'0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc',
|
|
23
|
+
'0x90f79bf6eb2c4f870365e785982e1f101e93b906',
|
|
24
|
+
'0x15d34aaf54267db7d7c367839aaf71a00a2c6a65',
|
|
25
|
+
'0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc',
|
|
26
|
+
'0x976ea74026e726554db657fa54763abd0c3a0aa9',
|
|
27
|
+
'0x14dc79964da2c08b23698b3d3cc7ca32193d9955',
|
|
28
|
+
'0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f',
|
|
29
|
+
'0xa0ee7a142d267c1f36714e4a8f75612f20a79720',
|
|
30
|
+
'0xbcd4042de499d14e55001ccbb24a551f3b954096',
|
|
31
|
+
'0x71be63f3384f5fb98995898a86b02fb2426c5788',
|
|
32
|
+
'0xfabb0ac9d68b0b445fb7357272ff202c5651694a',
|
|
33
|
+
'0x1cbd3b2770909d4e10f157cabc84c7264073c9ec',
|
|
34
|
+
'0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097',
|
|
35
|
+
'0xcd3b766ccdd6ae721141f452c550ca635964ce71',
|
|
36
|
+
'0x2546bcd3c84621e976d8185a91a922ae77ecec30',
|
|
37
|
+
'0xbda5747bfd65f08deb54cb465eb87d40e51b197e',
|
|
38
|
+
'0xdd2fd4581271e230360230f9337d5c0430bf44c0',
|
|
39
|
+
'0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199',
|
|
40
|
+
];
|
|
41
|
+
/** Private keys 1, 2 and 3 — the other convention for "obviously a test key". Only these
|
|
42
|
+
* three: a receipt signed with private key 4 is NOT flagged. */
|
|
43
|
+
const SMALL_SCALAR_ADDRESSES = [
|
|
44
|
+
'0x7e5f4552091a69125d5dfcb7b8c2659029395bdf',
|
|
45
|
+
'0x2b5ad5c4795c026514f8317c7a215e218dccd6cf',
|
|
46
|
+
'0x6813eb9362372eef6200f3b1dbc3f819671cba69',
|
|
47
|
+
];
|
|
48
|
+
/** lowercase address → the published secret it comes from. A Map, not an object literal: a
|
|
49
|
+
* lookup of 'constructor' or '__proto__' on a plain object answers with a prototype member. */
|
|
50
|
+
export const PUBLISHED_TEST_KEYS = new Map([
|
|
51
|
+
...DEV_MNEMONIC_ADDRESSES.map((a, i) => [a, `Hardhat/Anvil default dev mnemonic, account #${i}`]),
|
|
52
|
+
...SMALL_SCALAR_ADDRESSES.map((a, i) => [a, `private key 0x${(i + 1).toString(16)} (a small scalar)`]),
|
|
53
|
+
]);
|
|
54
|
+
/** The published-secret label for `address`, or undefined when it is not a known test key. */
|
|
55
|
+
export function publishedKeyLabel(address) {
|
|
56
|
+
if (typeof address !== 'string')
|
|
57
|
+
return undefined;
|
|
58
|
+
return PUBLISHED_TEST_KEYS.get(address.toLowerCase());
|
|
59
|
+
}
|
|
@@ -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;
|