x402check 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/cli.mjs +2353 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve as resolve$1 } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/types/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* Error and warning code vocabulary for x402check
|
|
8
|
+
*/
|
|
9
|
+
const ErrorCode = {
|
|
10
|
+
INVALID_JSON: "INVALID_JSON",
|
|
11
|
+
NOT_OBJECT: "NOT_OBJECT",
|
|
12
|
+
UNKNOWN_FORMAT: "UNKNOWN_FORMAT",
|
|
13
|
+
MISSING_VERSION: "MISSING_VERSION",
|
|
14
|
+
INVALID_VERSION: "INVALID_VERSION",
|
|
15
|
+
MISSING_ACCEPTS: "MISSING_ACCEPTS",
|
|
16
|
+
EMPTY_ACCEPTS: "EMPTY_ACCEPTS",
|
|
17
|
+
INVALID_ACCEPTS: "INVALID_ACCEPTS",
|
|
18
|
+
MISSING_SCHEME: "MISSING_SCHEME",
|
|
19
|
+
MISSING_NETWORK: "MISSING_NETWORK",
|
|
20
|
+
INVALID_NETWORK_FORMAT: "INVALID_NETWORK_FORMAT",
|
|
21
|
+
MISSING_AMOUNT: "MISSING_AMOUNT",
|
|
22
|
+
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
23
|
+
ZERO_AMOUNT: "ZERO_AMOUNT",
|
|
24
|
+
MISSING_ASSET: "MISSING_ASSET",
|
|
25
|
+
MISSING_PAY_TO: "MISSING_PAY_TO",
|
|
26
|
+
MISSING_RESOURCE: "MISSING_RESOURCE",
|
|
27
|
+
INVALID_URL: "INVALID_URL",
|
|
28
|
+
INVALID_TIMEOUT: "INVALID_TIMEOUT",
|
|
29
|
+
INVALID_EVM_ADDRESS: "INVALID_EVM_ADDRESS",
|
|
30
|
+
BAD_EVM_CHECKSUM: "BAD_EVM_CHECKSUM",
|
|
31
|
+
NO_EVM_CHECKSUM: "NO_EVM_CHECKSUM",
|
|
32
|
+
INVALID_SOLANA_ADDRESS: "INVALID_SOLANA_ADDRESS",
|
|
33
|
+
ADDRESS_NETWORK_MISMATCH: "ADDRESS_NETWORK_MISMATCH",
|
|
34
|
+
INVALID_BAZAAR_INFO: "INVALID_BAZAAR_INFO",
|
|
35
|
+
INVALID_BAZAAR_SCHEMA: "INVALID_BAZAAR_SCHEMA",
|
|
36
|
+
INVALID_BAZAAR_INFO_INPUT: "INVALID_BAZAAR_INFO_INPUT",
|
|
37
|
+
INVALID_OUTPUT_SCHEMA: "INVALID_OUTPUT_SCHEMA",
|
|
38
|
+
INVALID_OUTPUT_SCHEMA_INPUT: "INVALID_OUTPUT_SCHEMA_INPUT",
|
|
39
|
+
MISSING_INPUT_SCHEMA: "MISSING_INPUT_SCHEMA",
|
|
40
|
+
UNKNOWN_NETWORK: "UNKNOWN_NETWORK",
|
|
41
|
+
UNKNOWN_ASSET: "UNKNOWN_ASSET",
|
|
42
|
+
LEGACY_FORMAT: "LEGACY_FORMAT",
|
|
43
|
+
MISSING_MAX_TIMEOUT: "MISSING_MAX_TIMEOUT"
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Human-readable error messages for all error codes
|
|
47
|
+
*/
|
|
48
|
+
const ErrorMessages = {
|
|
49
|
+
INVALID_JSON: "Input is not valid JSON",
|
|
50
|
+
NOT_OBJECT: "Input must be an object",
|
|
51
|
+
UNKNOWN_FORMAT: "Missing required x402Version field (must be 1 or 2)",
|
|
52
|
+
MISSING_VERSION: "Missing required field: x402Version",
|
|
53
|
+
INVALID_VERSION: "Invalid x402Version value (must be 1 or 2)",
|
|
54
|
+
MISSING_ACCEPTS: "Missing required field: accepts",
|
|
55
|
+
EMPTY_ACCEPTS: "accepts array cannot be empty",
|
|
56
|
+
INVALID_ACCEPTS: "accepts must be an array",
|
|
57
|
+
MISSING_SCHEME: "Missing required field: scheme",
|
|
58
|
+
MISSING_NETWORK: "Missing required field: network",
|
|
59
|
+
INVALID_NETWORK_FORMAT: "Network must use CAIP-2 format (namespace:reference), e.g. eip155:8453",
|
|
60
|
+
MISSING_AMOUNT: "Missing required field: amount",
|
|
61
|
+
INVALID_AMOUNT: "Amount must be a numeric string in atomic units",
|
|
62
|
+
ZERO_AMOUNT: "Amount must be greater than zero",
|
|
63
|
+
MISSING_ASSET: "Missing required field: asset",
|
|
64
|
+
MISSING_PAY_TO: "Missing required field: payTo",
|
|
65
|
+
MISSING_RESOURCE: "Missing required field: resource",
|
|
66
|
+
INVALID_URL: "resource.url is not a valid URL format",
|
|
67
|
+
INVALID_TIMEOUT: "maxTimeoutSeconds must be a positive integer",
|
|
68
|
+
INVALID_EVM_ADDRESS: "Invalid EVM address format",
|
|
69
|
+
BAD_EVM_CHECKSUM: "EVM address has invalid checksum",
|
|
70
|
+
NO_EVM_CHECKSUM: "EVM address is all-lowercase with no checksum protection",
|
|
71
|
+
INVALID_SOLANA_ADDRESS: "Invalid Solana address format",
|
|
72
|
+
ADDRESS_NETWORK_MISMATCH: "Address format does not match network type",
|
|
73
|
+
INVALID_BAZAAR_INFO: "extensions.bazaar.info must be an object with input and output",
|
|
74
|
+
INVALID_BAZAAR_SCHEMA: "extensions.bazaar.schema must be a valid JSON Schema object",
|
|
75
|
+
INVALID_BAZAAR_INFO_INPUT: "extensions.bazaar.info.input must include type and method",
|
|
76
|
+
INVALID_OUTPUT_SCHEMA: "accepts[i].outputSchema must be an object with input and output",
|
|
77
|
+
INVALID_OUTPUT_SCHEMA_INPUT: "accepts[i].outputSchema.input must include type and method",
|
|
78
|
+
MISSING_INPUT_SCHEMA: "No input schema found (no bazaar extension or outputSchema) -- consider adding one so agents know how to call your API",
|
|
79
|
+
UNKNOWN_NETWORK: "Network is not in the known registry -- config may still work but cannot be fully validated",
|
|
80
|
+
UNKNOWN_ASSET: "Asset is not in the known registry -- config may still work but cannot be fully validated",
|
|
81
|
+
LEGACY_FORMAT: "Config uses legacy flat format -- consider upgrading to x402 v2",
|
|
82
|
+
MISSING_MAX_TIMEOUT: "Consider adding maxTimeoutSeconds for better security"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/types/parse-input.ts
|
|
87
|
+
/**
|
|
88
|
+
* Parse input that may be either a JSON string or an object
|
|
89
|
+
* API-04: Accept string | object
|
|
90
|
+
*/
|
|
91
|
+
function parseInput(input) {
|
|
92
|
+
if (typeof input === "string") try {
|
|
93
|
+
return { parsed: JSON.parse(input) };
|
|
94
|
+
} catch {
|
|
95
|
+
return {
|
|
96
|
+
parsed: null,
|
|
97
|
+
error: {
|
|
98
|
+
code: ErrorCode.INVALID_JSON,
|
|
99
|
+
field: "$",
|
|
100
|
+
message: ErrorMessages.INVALID_JSON,
|
|
101
|
+
severity: "error"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { parsed: input };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/detection/guards.ts
|
|
110
|
+
/**
|
|
111
|
+
* Check if value is a non-null, non-array object
|
|
112
|
+
*/
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if config has an accepts array
|
|
118
|
+
*/
|
|
119
|
+
function hasAcceptsArray(config) {
|
|
120
|
+
return "accepts" in config && Array.isArray(config.accepts);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Type guard for v2 config
|
|
124
|
+
* Checks for accepts array + x402Version: 2
|
|
125
|
+
* Note: resource is required by spec but its absence is a validation error, not a detection failure
|
|
126
|
+
*/
|
|
127
|
+
function isV2Config(value) {
|
|
128
|
+
if (!isRecord(value)) return false;
|
|
129
|
+
if (!hasAcceptsArray(value)) return false;
|
|
130
|
+
return "x402Version" in value && value.x402Version === 2;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Type guard for v1 config
|
|
134
|
+
* Checks for accepts array + x402Version: 1
|
|
135
|
+
*/
|
|
136
|
+
function isV1Config(value) {
|
|
137
|
+
if (!isRecord(value)) return false;
|
|
138
|
+
if (!hasAcceptsArray(value)) return false;
|
|
139
|
+
return "x402Version" in value && value.x402Version === 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/detection/detect.ts
|
|
144
|
+
/**
|
|
145
|
+
* Detect the format of an x402 config
|
|
146
|
+
*
|
|
147
|
+
* @param input - JSON string or parsed object
|
|
148
|
+
* @returns ConfigFormat literal: 'v2' | 'v1' | 'unknown'
|
|
149
|
+
*
|
|
150
|
+
* Detection requires x402Version field:
|
|
151
|
+
* 1. v2: accepts array + x402Version: 2
|
|
152
|
+
* 2. v1: accepts array + x402Version: 1
|
|
153
|
+
* 3. unknown: anything else (including versionless configs)
|
|
154
|
+
*/
|
|
155
|
+
function detect(input) {
|
|
156
|
+
const { parsed, error } = parseInput(input);
|
|
157
|
+
if (error) return "unknown";
|
|
158
|
+
if (isV2Config(parsed)) return "v2";
|
|
159
|
+
if (isV1Config(parsed)) return "v1";
|
|
160
|
+
return "unknown";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/detection/normalize.ts
|
|
165
|
+
/**
|
|
166
|
+
* Normalize any x402 config format to canonical v2 shape
|
|
167
|
+
*
|
|
168
|
+
* @param input - JSON string or parsed object
|
|
169
|
+
* @returns NormalizedConfig or null if format is unknown/invalid
|
|
170
|
+
*
|
|
171
|
+
* Normalization rules:
|
|
172
|
+
* - v2: Pass through with new object (FMT-07)
|
|
173
|
+
* - v1: Map maxAmountRequired → amount, lift per-entry resource (FMT-06)
|
|
174
|
+
* - unknown: Return null
|
|
175
|
+
*
|
|
176
|
+
* All transformations preserve extensions and extra fields (FMT-08)
|
|
177
|
+
*/
|
|
178
|
+
function normalize$1(input) {
|
|
179
|
+
const { parsed, error } = parseInput(input);
|
|
180
|
+
if (error) return null;
|
|
181
|
+
const format = detect(parsed);
|
|
182
|
+
switch (format) {
|
|
183
|
+
case "v2": return normalizeV2(parsed);
|
|
184
|
+
case "v1": return normalizeV1ToV2(parsed);
|
|
185
|
+
case "unknown": return null;
|
|
186
|
+
default: return format;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Normalize v2 config (pass-through with new object)
|
|
191
|
+
* FMT-07: v2 configs are already canonical, just create new object
|
|
192
|
+
*/
|
|
193
|
+
function normalizeV2(config) {
|
|
194
|
+
const result = {
|
|
195
|
+
x402Version: 2,
|
|
196
|
+
accepts: [...config.accepts],
|
|
197
|
+
resource: config.resource
|
|
198
|
+
};
|
|
199
|
+
if (config.error !== void 0) result.error = config.error;
|
|
200
|
+
if (config.extensions !== void 0) result.extensions = config.extensions;
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Normalize v1 config to v2
|
|
205
|
+
* FMT-06: Map maxAmountRequired → amount, lift per-entry resource to top level
|
|
206
|
+
*/
|
|
207
|
+
function normalizeV1ToV2(config) {
|
|
208
|
+
let topLevelResource = void 0;
|
|
209
|
+
const result = {
|
|
210
|
+
x402Version: 2,
|
|
211
|
+
accepts: config.accepts.map((entry) => {
|
|
212
|
+
if (entry.resource && !topLevelResource) topLevelResource = entry.resource;
|
|
213
|
+
const mapped = {
|
|
214
|
+
scheme: entry.scheme,
|
|
215
|
+
network: entry.network,
|
|
216
|
+
amount: entry.maxAmountRequired,
|
|
217
|
+
asset: entry.asset,
|
|
218
|
+
payTo: entry.payTo
|
|
219
|
+
};
|
|
220
|
+
if (entry.maxTimeoutSeconds !== void 0) mapped.maxTimeoutSeconds = entry.maxTimeoutSeconds;
|
|
221
|
+
if (entry.extra !== void 0) mapped.extra = entry.extra;
|
|
222
|
+
return mapped;
|
|
223
|
+
})
|
|
224
|
+
};
|
|
225
|
+
if (topLevelResource !== void 0) result.resource = topLevelResource;
|
|
226
|
+
if (config.error !== void 0) result.error = config.error;
|
|
227
|
+
if (config.extensions !== void 0) result.extensions = config.extensions;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/registries/networks.ts
|
|
233
|
+
const CAIP2_REGEX = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
|
|
234
|
+
const KNOWN_NETWORKS = {
|
|
235
|
+
"eip155:8453": {
|
|
236
|
+
name: "Base",
|
|
237
|
+
type: "evm",
|
|
238
|
+
testnet: false
|
|
239
|
+
},
|
|
240
|
+
"eip155:84532": {
|
|
241
|
+
name: "Base Sepolia",
|
|
242
|
+
type: "evm",
|
|
243
|
+
testnet: true
|
|
244
|
+
},
|
|
245
|
+
"eip155:43114": {
|
|
246
|
+
name: "Avalanche C-Chain",
|
|
247
|
+
type: "evm",
|
|
248
|
+
testnet: false
|
|
249
|
+
},
|
|
250
|
+
"eip155:43113": {
|
|
251
|
+
name: "Avalanche Fuji",
|
|
252
|
+
type: "evm",
|
|
253
|
+
testnet: true
|
|
254
|
+
},
|
|
255
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": {
|
|
256
|
+
name: "Solana Mainnet",
|
|
257
|
+
type: "solana",
|
|
258
|
+
testnet: false
|
|
259
|
+
},
|
|
260
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": {
|
|
261
|
+
name: "Solana Devnet",
|
|
262
|
+
type: "solana",
|
|
263
|
+
testnet: true
|
|
264
|
+
},
|
|
265
|
+
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z": {
|
|
266
|
+
name: "Solana Testnet",
|
|
267
|
+
type: "solana",
|
|
268
|
+
testnet: true
|
|
269
|
+
},
|
|
270
|
+
"stellar:pubnet": {
|
|
271
|
+
name: "Stellar Mainnet",
|
|
272
|
+
type: "stellar",
|
|
273
|
+
testnet: false
|
|
274
|
+
},
|
|
275
|
+
"stellar:testnet": {
|
|
276
|
+
name: "Stellar Testnet",
|
|
277
|
+
type: "stellar",
|
|
278
|
+
testnet: true
|
|
279
|
+
},
|
|
280
|
+
"aptos:1": {
|
|
281
|
+
name: "Aptos Mainnet",
|
|
282
|
+
type: "aptos",
|
|
283
|
+
testnet: false
|
|
284
|
+
},
|
|
285
|
+
"aptos:2": {
|
|
286
|
+
name: "Aptos Testnet",
|
|
287
|
+
type: "aptos",
|
|
288
|
+
testnet: true
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function isValidCaip2(value) {
|
|
292
|
+
return CAIP2_REGEX.test(value);
|
|
293
|
+
}
|
|
294
|
+
function isKnownNetwork(caip2) {
|
|
295
|
+
return caip2 in KNOWN_NETWORKS;
|
|
296
|
+
}
|
|
297
|
+
function getNetworkInfo(caip2) {
|
|
298
|
+
return KNOWN_NETWORKS[caip2];
|
|
299
|
+
}
|
|
300
|
+
function getNetworkNamespace(caip2) {
|
|
301
|
+
if (!isValidCaip2(caip2)) return;
|
|
302
|
+
const colonIndex = caip2.indexOf(":");
|
|
303
|
+
return colonIndex > 0 ? caip2.substring(0, colonIndex) : void 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region ../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/_u64.js
|
|
308
|
+
/**
|
|
309
|
+
* Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
|
|
310
|
+
* @todo re-check https://issues.chromium.org/issues/42212588
|
|
311
|
+
* @module
|
|
312
|
+
*/
|
|
313
|
+
const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
|
314
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
315
|
+
function fromBig(n, le = false) {
|
|
316
|
+
if (le) return {
|
|
317
|
+
h: Number(n & U32_MASK64),
|
|
318
|
+
l: Number(n >> _32n & U32_MASK64)
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
h: Number(n >> _32n & U32_MASK64) | 0,
|
|
322
|
+
l: Number(n & U32_MASK64) | 0
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function split(lst, le = false) {
|
|
326
|
+
const len = lst.length;
|
|
327
|
+
let Ah = new Uint32Array(len);
|
|
328
|
+
let Al = new Uint32Array(len);
|
|
329
|
+
for (let i = 0; i < len; i++) {
|
|
330
|
+
const { h, l } = fromBig(lst[i], le);
|
|
331
|
+
[Ah[i], Al[i]] = [h, l];
|
|
332
|
+
}
|
|
333
|
+
return [Ah, Al];
|
|
334
|
+
}
|
|
335
|
+
const rotlSH = (h, l, s) => h << s | l >>> 32 - s;
|
|
336
|
+
const rotlSL = (h, l, s) => l << s | h >>> 32 - s;
|
|
337
|
+
const rotlBH = (h, l, s) => l << s - 32 | h >>> 64 - s;
|
|
338
|
+
const rotlBL = (h, l, s) => h << s - 32 | l >>> 64 - s;
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region ../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/utils.js
|
|
342
|
+
/**
|
|
343
|
+
* Utilities for hex, bytes, CSPRNG.
|
|
344
|
+
* @module
|
|
345
|
+
*/
|
|
346
|
+
/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
347
|
+
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
348
|
+
function isBytes$1(a) {
|
|
349
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
350
|
+
}
|
|
351
|
+
/** Asserts something is positive integer. */
|
|
352
|
+
function anumber$1(n, title = "") {
|
|
353
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
354
|
+
const prefix = title && `"${title}" `;
|
|
355
|
+
throw new Error(`${prefix}expected integer >= 0, got ${n}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Asserts something is Uint8Array. */
|
|
359
|
+
function abytes$1(value, length, title = "") {
|
|
360
|
+
const bytes = isBytes$1(value);
|
|
361
|
+
const len = value?.length;
|
|
362
|
+
const needsLen = length !== void 0;
|
|
363
|
+
if (!bytes || needsLen && len !== length) {
|
|
364
|
+
const prefix = title && `"${title}" `;
|
|
365
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
366
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
367
|
+
throw new Error(prefix + "expected Uint8Array" + ofLen + ", got " + got);
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
/** Asserts a hash instance has not been destroyed / finished */
|
|
372
|
+
function aexists(instance, checkFinished = true) {
|
|
373
|
+
if (instance.destroyed) throw new Error("Hash instance has been destroyed");
|
|
374
|
+
if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
|
|
375
|
+
}
|
|
376
|
+
/** Asserts output is properly-sized byte array */
|
|
377
|
+
function aoutput(out, instance) {
|
|
378
|
+
abytes$1(out, void 0, "digestInto() output");
|
|
379
|
+
const min = instance.outputLen;
|
|
380
|
+
if (out.length < min) throw new Error("\"digestInto() output\" expected to be of length >=" + min);
|
|
381
|
+
}
|
|
382
|
+
/** Cast u8 / u16 / u32 to u32. */
|
|
383
|
+
function u32(arr) {
|
|
384
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
385
|
+
}
|
|
386
|
+
/** Zeroize a byte array. Warning: JS provides no guarantees. */
|
|
387
|
+
function clean(...arrays) {
|
|
388
|
+
for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
|
|
389
|
+
}
|
|
390
|
+
/** Is current platform little-endian? Most are. Big-Endian platform: IBM */
|
|
391
|
+
const isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68;
|
|
392
|
+
/** The byte swap operation for uint32 */
|
|
393
|
+
function byteSwap(word) {
|
|
394
|
+
return word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255;
|
|
395
|
+
}
|
|
396
|
+
/** In place byte swap for Uint32Array */
|
|
397
|
+
function byteSwap32(arr) {
|
|
398
|
+
for (let i = 0; i < arr.length; i++) arr[i] = byteSwap(arr[i]);
|
|
399
|
+
return arr;
|
|
400
|
+
}
|
|
401
|
+
const swap32IfBE = isLE ? (u) => u : byteSwap32;
|
|
402
|
+
const hasHexBuiltin$1 = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
|
|
403
|
+
/** Creates function with outputLen, blockLen, create properties from a class constructor. */
|
|
404
|
+
function createHasher(hashCons, info = {}) {
|
|
405
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
406
|
+
const tmp = hashCons(void 0);
|
|
407
|
+
hashC.outputLen = tmp.outputLen;
|
|
408
|
+
hashC.blockLen = tmp.blockLen;
|
|
409
|
+
hashC.create = (opts) => hashCons(opts);
|
|
410
|
+
Object.assign(hashC, info);
|
|
411
|
+
return Object.freeze(hashC);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region ../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/sha3.js
|
|
416
|
+
/**
|
|
417
|
+
* SHA3 (keccak) hash function, based on a new "Sponge function" design.
|
|
418
|
+
* Different from older hashes, the internal state is bigger than output size.
|
|
419
|
+
*
|
|
420
|
+
* Check out [FIPS-202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf),
|
|
421
|
+
* [Website](https://keccak.team/keccak.html),
|
|
422
|
+
* [the differences between SHA-3 and Keccak](https://crypto.stackexchange.com/questions/15727/what-are-the-key-differences-between-the-draft-sha-3-standard-and-the-keccak-sub).
|
|
423
|
+
*
|
|
424
|
+
* Check out `sha3-addons` module for cSHAKE, k12, and others.
|
|
425
|
+
* @module
|
|
426
|
+
*/
|
|
427
|
+
const _0n = BigInt(0);
|
|
428
|
+
const _1n = BigInt(1);
|
|
429
|
+
const _2n = BigInt(2);
|
|
430
|
+
const _7n = BigInt(7);
|
|
431
|
+
const _256n = BigInt(256);
|
|
432
|
+
const _0x71n = BigInt(113);
|
|
433
|
+
const SHA3_PI = [];
|
|
434
|
+
const SHA3_ROTL = [];
|
|
435
|
+
const _SHA3_IOTA = [];
|
|
436
|
+
for (let round = 0, R = _1n, x = 1, y = 0; round < 24; round++) {
|
|
437
|
+
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
438
|
+
SHA3_PI.push(2 * (5 * y + x));
|
|
439
|
+
SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
|
|
440
|
+
let t = _0n;
|
|
441
|
+
for (let j = 0; j < 7; j++) {
|
|
442
|
+
R = (R << _1n ^ (R >> _7n) * _0x71n) % _256n;
|
|
443
|
+
if (R & _2n) t ^= _1n << (_1n << BigInt(j)) - _1n;
|
|
444
|
+
}
|
|
445
|
+
_SHA3_IOTA.push(t);
|
|
446
|
+
}
|
|
447
|
+
const IOTAS = split(_SHA3_IOTA, true);
|
|
448
|
+
const SHA3_IOTA_H = IOTAS[0];
|
|
449
|
+
const SHA3_IOTA_L = IOTAS[1];
|
|
450
|
+
const rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
|
|
451
|
+
const rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
|
|
452
|
+
/** `keccakf1600` internal function, additionally allows to adjust round count. */
|
|
453
|
+
function keccakP(s, rounds = 24) {
|
|
454
|
+
const B = new Uint32Array(10);
|
|
455
|
+
for (let round = 24 - rounds; round < 24; round++) {
|
|
456
|
+
for (let x = 0; x < 10; x++) B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
|
|
457
|
+
for (let x = 0; x < 10; x += 2) {
|
|
458
|
+
const idx1 = (x + 8) % 10;
|
|
459
|
+
const idx0 = (x + 2) % 10;
|
|
460
|
+
const B0 = B[idx0];
|
|
461
|
+
const B1 = B[idx0 + 1];
|
|
462
|
+
const Th = rotlH(B0, B1, 1) ^ B[idx1];
|
|
463
|
+
const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
|
|
464
|
+
for (let y = 0; y < 50; y += 10) {
|
|
465
|
+
s[x + y] ^= Th;
|
|
466
|
+
s[x + y + 1] ^= Tl;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
let curH = s[2];
|
|
470
|
+
let curL = s[3];
|
|
471
|
+
for (let t = 0; t < 24; t++) {
|
|
472
|
+
const shift = SHA3_ROTL[t];
|
|
473
|
+
const Th = rotlH(curH, curL, shift);
|
|
474
|
+
const Tl = rotlL(curH, curL, shift);
|
|
475
|
+
const PI = SHA3_PI[t];
|
|
476
|
+
curH = s[PI];
|
|
477
|
+
curL = s[PI + 1];
|
|
478
|
+
s[PI] = Th;
|
|
479
|
+
s[PI + 1] = Tl;
|
|
480
|
+
}
|
|
481
|
+
for (let y = 0; y < 50; y += 10) {
|
|
482
|
+
for (let x = 0; x < 10; x++) B[x] = s[y + x];
|
|
483
|
+
for (let x = 0; x < 10; x++) s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
|
|
484
|
+
}
|
|
485
|
+
s[0] ^= SHA3_IOTA_H[round];
|
|
486
|
+
s[1] ^= SHA3_IOTA_L[round];
|
|
487
|
+
}
|
|
488
|
+
clean(B);
|
|
489
|
+
}
|
|
490
|
+
/** Keccak sponge function. */
|
|
491
|
+
var Keccak = class Keccak {
|
|
492
|
+
state;
|
|
493
|
+
pos = 0;
|
|
494
|
+
posOut = 0;
|
|
495
|
+
finished = false;
|
|
496
|
+
state32;
|
|
497
|
+
destroyed = false;
|
|
498
|
+
blockLen;
|
|
499
|
+
suffix;
|
|
500
|
+
outputLen;
|
|
501
|
+
enableXOF = false;
|
|
502
|
+
rounds;
|
|
503
|
+
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
|
|
504
|
+
this.blockLen = blockLen;
|
|
505
|
+
this.suffix = suffix;
|
|
506
|
+
this.outputLen = outputLen;
|
|
507
|
+
this.enableXOF = enableXOF;
|
|
508
|
+
this.rounds = rounds;
|
|
509
|
+
anumber$1(outputLen, "outputLen");
|
|
510
|
+
if (!(0 < blockLen && blockLen < 200)) throw new Error("only keccak-f1600 function is supported");
|
|
511
|
+
this.state = new Uint8Array(200);
|
|
512
|
+
this.state32 = u32(this.state);
|
|
513
|
+
}
|
|
514
|
+
clone() {
|
|
515
|
+
return this._cloneInto();
|
|
516
|
+
}
|
|
517
|
+
keccak() {
|
|
518
|
+
swap32IfBE(this.state32);
|
|
519
|
+
keccakP(this.state32, this.rounds);
|
|
520
|
+
swap32IfBE(this.state32);
|
|
521
|
+
this.posOut = 0;
|
|
522
|
+
this.pos = 0;
|
|
523
|
+
}
|
|
524
|
+
update(data) {
|
|
525
|
+
aexists(this);
|
|
526
|
+
abytes$1(data);
|
|
527
|
+
const { blockLen, state } = this;
|
|
528
|
+
const len = data.length;
|
|
529
|
+
for (let pos = 0; pos < len;) {
|
|
530
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
531
|
+
for (let i = 0; i < take; i++) state[this.pos++] ^= data[pos++];
|
|
532
|
+
if (this.pos === blockLen) this.keccak();
|
|
533
|
+
}
|
|
534
|
+
return this;
|
|
535
|
+
}
|
|
536
|
+
finish() {
|
|
537
|
+
if (this.finished) return;
|
|
538
|
+
this.finished = true;
|
|
539
|
+
const { state, suffix, pos, blockLen } = this;
|
|
540
|
+
state[pos] ^= suffix;
|
|
541
|
+
if ((suffix & 128) !== 0 && pos === blockLen - 1) this.keccak();
|
|
542
|
+
state[blockLen - 1] ^= 128;
|
|
543
|
+
this.keccak();
|
|
544
|
+
}
|
|
545
|
+
writeInto(out) {
|
|
546
|
+
aexists(this, false);
|
|
547
|
+
abytes$1(out);
|
|
548
|
+
this.finish();
|
|
549
|
+
const bufferOut = this.state;
|
|
550
|
+
const { blockLen } = this;
|
|
551
|
+
for (let pos = 0, len = out.length; pos < len;) {
|
|
552
|
+
if (this.posOut >= blockLen) this.keccak();
|
|
553
|
+
const take = Math.min(blockLen - this.posOut, len - pos);
|
|
554
|
+
out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
|
|
555
|
+
this.posOut += take;
|
|
556
|
+
pos += take;
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
xofInto(out) {
|
|
561
|
+
if (!this.enableXOF) throw new Error("XOF is not possible for this instance");
|
|
562
|
+
return this.writeInto(out);
|
|
563
|
+
}
|
|
564
|
+
xof(bytes) {
|
|
565
|
+
anumber$1(bytes);
|
|
566
|
+
return this.xofInto(new Uint8Array(bytes));
|
|
567
|
+
}
|
|
568
|
+
digestInto(out) {
|
|
569
|
+
aoutput(out, this);
|
|
570
|
+
if (this.finished) throw new Error("digest() was already called");
|
|
571
|
+
this.writeInto(out);
|
|
572
|
+
this.destroy();
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
digest() {
|
|
576
|
+
return this.digestInto(new Uint8Array(this.outputLen));
|
|
577
|
+
}
|
|
578
|
+
destroy() {
|
|
579
|
+
this.destroyed = true;
|
|
580
|
+
clean(this.state);
|
|
581
|
+
}
|
|
582
|
+
_cloneInto(to) {
|
|
583
|
+
const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
|
|
584
|
+
to ||= new Keccak(blockLen, suffix, outputLen, enableXOF, rounds);
|
|
585
|
+
to.state32.set(this.state32);
|
|
586
|
+
to.pos = this.pos;
|
|
587
|
+
to.posOut = this.posOut;
|
|
588
|
+
to.finished = this.finished;
|
|
589
|
+
to.rounds = rounds;
|
|
590
|
+
to.suffix = suffix;
|
|
591
|
+
to.outputLen = outputLen;
|
|
592
|
+
to.enableXOF = enableXOF;
|
|
593
|
+
to.destroyed = this.destroyed;
|
|
594
|
+
return to;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const genKeccak = (suffix, blockLen, outputLen, info = {}) => createHasher(() => new Keccak(blockLen, suffix, outputLen), info);
|
|
598
|
+
/** keccak-256 hash function. Different from SHA3-256. */
|
|
599
|
+
const keccak_256 = /* @__PURE__ */ genKeccak(1, 136, 32);
|
|
600
|
+
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/crypto/keccak256.ts
|
|
603
|
+
/**
|
|
604
|
+
* Keccak-256 hash function wrapper
|
|
605
|
+
* Uses @noble/hashes for audited, tree-shakeable implementation
|
|
606
|
+
*/
|
|
607
|
+
/**
|
|
608
|
+
* Compute Keccak-256 hash (NOT SHA-3)
|
|
609
|
+
*
|
|
610
|
+
* @param input - String or Uint8Array to hash
|
|
611
|
+
* @returns Lowercase hex string (64 chars, no 0x prefix)
|
|
612
|
+
*/
|
|
613
|
+
function keccak256(input) {
|
|
614
|
+
const hash = keccak_256(typeof input === "string" ? new TextEncoder().encode(input) : input);
|
|
615
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/crypto/eip55.ts
|
|
620
|
+
/**
|
|
621
|
+
* EIP-55 mixed-case checksum address encoding
|
|
622
|
+
* Spec: https://eips.ethereum.org/EIPS/eip-55
|
|
623
|
+
*/
|
|
624
|
+
/**
|
|
625
|
+
* Convert an Ethereum address to EIP-55 checksummed format
|
|
626
|
+
*
|
|
627
|
+
* @param address - 42-character hex address (0x-prefixed)
|
|
628
|
+
* @returns Checksummed address with mixed case
|
|
629
|
+
*/
|
|
630
|
+
function toChecksumAddress(address) {
|
|
631
|
+
const lowerHex = address.slice(2).toLowerCase();
|
|
632
|
+
const hash = keccak256(lowerHex);
|
|
633
|
+
let result = "0x";
|
|
634
|
+
for (let i = 0; i < lowerHex.length; i++) {
|
|
635
|
+
const char = lowerHex[i];
|
|
636
|
+
const hashChar = hash[i];
|
|
637
|
+
if (!char || !hashChar) continue;
|
|
638
|
+
if (char >= "a" && char <= "f") result += parseInt(hashChar, 16) >= 8 ? char.toUpperCase() : char;
|
|
639
|
+
else result += char;
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Check if an address has valid EIP-55 checksum
|
|
645
|
+
*
|
|
646
|
+
* Returns false for all-lowercase or all-uppercase addresses
|
|
647
|
+
* (these are valid formats but do not match their checksummed version)
|
|
648
|
+
*
|
|
649
|
+
* @param address - Address to validate
|
|
650
|
+
* @returns True if checksum is valid
|
|
651
|
+
*/
|
|
652
|
+
function isValidChecksum(address) {
|
|
653
|
+
return address === toChecksumAddress(address);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/validation/evm-address.ts
|
|
658
|
+
/**
|
|
659
|
+
* EVM address validation with EIP-55 checksum verification
|
|
660
|
+
*/
|
|
661
|
+
const EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/;
|
|
662
|
+
/**
|
|
663
|
+
* Validate an EVM address format and checksum
|
|
664
|
+
*
|
|
665
|
+
* Returns errors for invalid format, warnings for checksum issues
|
|
666
|
+
*
|
|
667
|
+
* @param address - Address to validate
|
|
668
|
+
* @param field - Field path for error reporting
|
|
669
|
+
* @returns Array of validation issues (empty if valid)
|
|
670
|
+
*/
|
|
671
|
+
function validateEvmAddress(address, field) {
|
|
672
|
+
if (!EVM_ADDRESS_REGEX.test(address)) return [{
|
|
673
|
+
code: ErrorCode.INVALID_EVM_ADDRESS,
|
|
674
|
+
field,
|
|
675
|
+
message: "EVM address must be 42 hex characters with 0x prefix",
|
|
676
|
+
severity: "error",
|
|
677
|
+
fix: "Format: 0x followed by 40 hex digits (0-9, a-f, A-F)"
|
|
678
|
+
}];
|
|
679
|
+
const hexPart = address.slice(2);
|
|
680
|
+
if (address === address.toLowerCase() && /[a-f]/.test(hexPart)) return [{
|
|
681
|
+
code: ErrorCode.NO_EVM_CHECKSUM,
|
|
682
|
+
field,
|
|
683
|
+
message: "EVM address is all-lowercase with no checksum protection",
|
|
684
|
+
severity: "warning",
|
|
685
|
+
fix: `Use checksummed address to detect typos: ${toChecksumAddress(address)}`
|
|
686
|
+
}];
|
|
687
|
+
if (/^[0-9A-F]{40}$/.test(hexPart) && /[A-F]/.test(hexPart)) return [];
|
|
688
|
+
if (/^[0-9]{40}$/.test(hexPart)) return [];
|
|
689
|
+
if (!isValidChecksum(address)) return [{
|
|
690
|
+
code: ErrorCode.BAD_EVM_CHECKSUM,
|
|
691
|
+
field,
|
|
692
|
+
message: "EVM address has invalid checksum (EIP-55)",
|
|
693
|
+
severity: "warning",
|
|
694
|
+
fix: `Expected: ${toChecksumAddress(address)}`
|
|
695
|
+
}];
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region ../../node_modules/.pnpm/@scure+base@2.0.0/node_modules/@scure/base/index.js
|
|
701
|
+
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
702
|
+
function isBytes(a) {
|
|
703
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
704
|
+
}
|
|
705
|
+
/** Asserts something is Uint8Array. */
|
|
706
|
+
function abytes(b) {
|
|
707
|
+
if (!isBytes(b)) throw new Error("Uint8Array expected");
|
|
708
|
+
}
|
|
709
|
+
function isArrayOf(isString, arr) {
|
|
710
|
+
if (!Array.isArray(arr)) return false;
|
|
711
|
+
if (arr.length === 0) return true;
|
|
712
|
+
if (isString) return arr.every((item) => typeof item === "string");
|
|
713
|
+
else return arr.every((item) => Number.isSafeInteger(item));
|
|
714
|
+
}
|
|
715
|
+
function afn(input) {
|
|
716
|
+
if (typeof input !== "function") throw new Error("function expected");
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
function astr(label, input) {
|
|
720
|
+
if (typeof input !== "string") throw new Error(`${label}: string expected`);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
function anumber(n) {
|
|
724
|
+
if (!Number.isSafeInteger(n)) throw new Error(`invalid integer: ${n}`);
|
|
725
|
+
}
|
|
726
|
+
function aArr(input) {
|
|
727
|
+
if (!Array.isArray(input)) throw new Error("array expected");
|
|
728
|
+
}
|
|
729
|
+
function astrArr(label, input) {
|
|
730
|
+
if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`);
|
|
731
|
+
}
|
|
732
|
+
function anumArr(label, input) {
|
|
733
|
+
if (!isArrayOf(false, input)) throw new Error(`${label}: array of numbers expected`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* @__NO_SIDE_EFFECTS__
|
|
737
|
+
*/
|
|
738
|
+
function chain(...args) {
|
|
739
|
+
const id = (a) => a;
|
|
740
|
+
const wrap = (a, b) => (c) => a(b(c));
|
|
741
|
+
return {
|
|
742
|
+
encode: args.map((x) => x.encode).reduceRight(wrap, id),
|
|
743
|
+
decode: args.map((x) => x.decode).reduce(wrap, id)
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Encodes integer radix representation to array of strings using alphabet and back.
|
|
748
|
+
* Could also be array of strings.
|
|
749
|
+
* @__NO_SIDE_EFFECTS__
|
|
750
|
+
*/
|
|
751
|
+
function alphabet(letters) {
|
|
752
|
+
const lettersA = typeof letters === "string" ? letters.split("") : letters;
|
|
753
|
+
const len = lettersA.length;
|
|
754
|
+
astrArr("alphabet", lettersA);
|
|
755
|
+
const indexes = new Map(lettersA.map((l, i) => [l, i]));
|
|
756
|
+
return {
|
|
757
|
+
encode: (digits) => {
|
|
758
|
+
aArr(digits);
|
|
759
|
+
return digits.map((i) => {
|
|
760
|
+
if (!Number.isSafeInteger(i) || i < 0 || i >= len) throw new Error(`alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}`);
|
|
761
|
+
return lettersA[i];
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
decode: (input) => {
|
|
765
|
+
aArr(input);
|
|
766
|
+
return input.map((letter) => {
|
|
767
|
+
astr("alphabet.decode", letter);
|
|
768
|
+
const i = indexes.get(letter);
|
|
769
|
+
if (i === void 0) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`);
|
|
770
|
+
return i;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* @__NO_SIDE_EFFECTS__
|
|
777
|
+
*/
|
|
778
|
+
function join(separator = "") {
|
|
779
|
+
astr("join", separator);
|
|
780
|
+
return {
|
|
781
|
+
encode: (from) => {
|
|
782
|
+
astrArr("join.decode", from);
|
|
783
|
+
return from.join(separator);
|
|
784
|
+
},
|
|
785
|
+
decode: (to) => {
|
|
786
|
+
astr("join.decode", to);
|
|
787
|
+
return to.split(separator);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Pad strings array so it has integer number of bits
|
|
793
|
+
* @__NO_SIDE_EFFECTS__
|
|
794
|
+
*/
|
|
795
|
+
function padding(bits, chr = "=") {
|
|
796
|
+
anumber(bits);
|
|
797
|
+
astr("padding", chr);
|
|
798
|
+
return {
|
|
799
|
+
encode(data) {
|
|
800
|
+
astrArr("padding.encode", data);
|
|
801
|
+
while (data.length * bits % 8) data.push(chr);
|
|
802
|
+
return data;
|
|
803
|
+
},
|
|
804
|
+
decode(input) {
|
|
805
|
+
astrArr("padding.decode", input);
|
|
806
|
+
let end = input.length;
|
|
807
|
+
if (end * bits % 8) throw new Error("padding: invalid, string should have whole number of bytes");
|
|
808
|
+
for (; end > 0 && input[end - 1] === chr; end--) if ((end - 1) * bits % 8 === 0) throw new Error("padding: invalid, string has too much padding");
|
|
809
|
+
return input.slice(0, end);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* @__NO_SIDE_EFFECTS__
|
|
815
|
+
*/
|
|
816
|
+
function normalize(fn) {
|
|
817
|
+
afn(fn);
|
|
818
|
+
return {
|
|
819
|
+
encode: (from) => from,
|
|
820
|
+
decode: (to) => fn(to)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Slow: O(n^2) time complexity
|
|
825
|
+
*/
|
|
826
|
+
function convertRadix(data, from, to) {
|
|
827
|
+
if (from < 2) throw new Error(`convertRadix: invalid from=${from}, base cannot be less than 2`);
|
|
828
|
+
if (to < 2) throw new Error(`convertRadix: invalid to=${to}, base cannot be less than 2`);
|
|
829
|
+
aArr(data);
|
|
830
|
+
if (!data.length) return [];
|
|
831
|
+
let pos = 0;
|
|
832
|
+
const res = [];
|
|
833
|
+
const digits = Array.from(data, (d) => {
|
|
834
|
+
anumber(d);
|
|
835
|
+
if (d < 0 || d >= from) throw new Error(`invalid integer: ${d}`);
|
|
836
|
+
return d;
|
|
837
|
+
});
|
|
838
|
+
const dlen = digits.length;
|
|
839
|
+
while (true) {
|
|
840
|
+
let carry = 0;
|
|
841
|
+
let done = true;
|
|
842
|
+
for (let i = pos; i < dlen; i++) {
|
|
843
|
+
const digit = digits[i];
|
|
844
|
+
const fromCarry = from * carry;
|
|
845
|
+
const digitBase = fromCarry + digit;
|
|
846
|
+
if (!Number.isSafeInteger(digitBase) || fromCarry / from !== carry || digitBase - digit !== fromCarry) throw new Error("convertRadix: carry overflow");
|
|
847
|
+
const div = digitBase / to;
|
|
848
|
+
carry = digitBase % to;
|
|
849
|
+
const rounded = Math.floor(div);
|
|
850
|
+
digits[i] = rounded;
|
|
851
|
+
if (!Number.isSafeInteger(rounded) || rounded * to + carry !== digitBase) throw new Error("convertRadix: carry overflow");
|
|
852
|
+
if (!done) continue;
|
|
853
|
+
else if (!rounded) pos = i;
|
|
854
|
+
else done = false;
|
|
855
|
+
}
|
|
856
|
+
res.push(carry);
|
|
857
|
+
if (done) break;
|
|
858
|
+
}
|
|
859
|
+
for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0);
|
|
860
|
+
return res.reverse();
|
|
861
|
+
}
|
|
862
|
+
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
863
|
+
const radix2carry = /* @__NO_SIDE_EFFECTS__ */ (from, to) => from + (to - gcd(from, to));
|
|
864
|
+
const powers = /* @__PURE__ */ (() => {
|
|
865
|
+
let res = [];
|
|
866
|
+
for (let i = 0; i < 40; i++) res.push(2 ** i);
|
|
867
|
+
return res;
|
|
868
|
+
})();
|
|
869
|
+
/**
|
|
870
|
+
* Implemented with numbers, because BigInt is 5x slower
|
|
871
|
+
*/
|
|
872
|
+
function convertRadix2(data, from, to, padding) {
|
|
873
|
+
aArr(data);
|
|
874
|
+
if (from <= 0 || from > 32) throw new Error(`convertRadix2: wrong from=${from}`);
|
|
875
|
+
if (to <= 0 || to > 32) throw new Error(`convertRadix2: wrong to=${to}`);
|
|
876
|
+
if (/* @__PURE__ */ radix2carry(from, to) > 32) throw new Error(`convertRadix2: carry overflow from=${from} to=${to} carryBits=${/* @__PURE__ */ radix2carry(from, to)}`);
|
|
877
|
+
let carry = 0;
|
|
878
|
+
let pos = 0;
|
|
879
|
+
const max = powers[from];
|
|
880
|
+
const mask = powers[to] - 1;
|
|
881
|
+
const res = [];
|
|
882
|
+
for (const n of data) {
|
|
883
|
+
anumber(n);
|
|
884
|
+
if (n >= max) throw new Error(`convertRadix2: invalid data word=${n} from=${from}`);
|
|
885
|
+
carry = carry << from | n;
|
|
886
|
+
if (pos + from > 32) throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`);
|
|
887
|
+
pos += from;
|
|
888
|
+
for (; pos >= to; pos -= to) res.push((carry >> pos - to & mask) >>> 0);
|
|
889
|
+
const pow = powers[pos];
|
|
890
|
+
if (pow === void 0) throw new Error("invalid carry");
|
|
891
|
+
carry &= pow - 1;
|
|
892
|
+
}
|
|
893
|
+
carry = carry << to - pos & mask;
|
|
894
|
+
if (!padding && pos >= from) throw new Error("Excess padding");
|
|
895
|
+
if (!padding && carry > 0) throw new Error(`Non-zero padding: ${carry}`);
|
|
896
|
+
if (padding && pos > 0) res.push(carry >>> 0);
|
|
897
|
+
return res;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* @__NO_SIDE_EFFECTS__
|
|
901
|
+
*/
|
|
902
|
+
function radix(num) {
|
|
903
|
+
anumber(num);
|
|
904
|
+
const _256 = 2 ** 8;
|
|
905
|
+
return {
|
|
906
|
+
encode: (bytes) => {
|
|
907
|
+
if (!isBytes(bytes)) throw new Error("radix.encode input should be Uint8Array");
|
|
908
|
+
return convertRadix(Array.from(bytes), _256, num);
|
|
909
|
+
},
|
|
910
|
+
decode: (digits) => {
|
|
911
|
+
anumArr("radix.decode", digits);
|
|
912
|
+
return Uint8Array.from(convertRadix(digits, num, _256));
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* If both bases are power of same number (like `2**8 <-> 2**64`),
|
|
918
|
+
* there is a linear algorithm. For now we have implementation for power-of-two bases only.
|
|
919
|
+
* @__NO_SIDE_EFFECTS__
|
|
920
|
+
*/
|
|
921
|
+
function radix2(bits, revPadding = false) {
|
|
922
|
+
anumber(bits);
|
|
923
|
+
if (bits <= 0 || bits > 32) throw new Error("radix2: bits should be in (0..32]");
|
|
924
|
+
if (/* @__PURE__ */ radix2carry(8, bits) > 32 || /* @__PURE__ */ radix2carry(bits, 8) > 32) throw new Error("radix2: carry overflow");
|
|
925
|
+
return {
|
|
926
|
+
encode: (bytes) => {
|
|
927
|
+
if (!isBytes(bytes)) throw new Error("radix2.encode input should be Uint8Array");
|
|
928
|
+
return convertRadix2(Array.from(bytes), 8, bits, !revPadding);
|
|
929
|
+
},
|
|
930
|
+
decode: (digits) => {
|
|
931
|
+
anumArr("radix2.decode", digits);
|
|
932
|
+
return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding));
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function unsafeWrapper(fn) {
|
|
937
|
+
afn(fn);
|
|
938
|
+
return function(...args) {
|
|
939
|
+
try {
|
|
940
|
+
return fn.apply(null, args);
|
|
941
|
+
} catch (e) {}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* base16 encoding from RFC 4648.
|
|
946
|
+
* @example
|
|
947
|
+
* ```js
|
|
948
|
+
* base16.encode(Uint8Array.from([0x12, 0xab]));
|
|
949
|
+
* // => '12AB'
|
|
950
|
+
* ```
|
|
951
|
+
*/
|
|
952
|
+
const base16 = chain(radix2(4), alphabet("0123456789ABCDEF"), join(""));
|
|
953
|
+
/**
|
|
954
|
+
* base32 encoding from RFC 4648. Has padding.
|
|
955
|
+
* Use `base32nopad` for unpadded version.
|
|
956
|
+
* Also check out `base32hex`, `base32hexnopad`, `base32crockford`.
|
|
957
|
+
* @example
|
|
958
|
+
* ```js
|
|
959
|
+
* base32.encode(Uint8Array.from([0x12, 0xab]));
|
|
960
|
+
* // => 'CKVQ===='
|
|
961
|
+
* base32.decode('CKVQ====');
|
|
962
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
963
|
+
* ```
|
|
964
|
+
*/
|
|
965
|
+
const base32 = chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), padding(5), join(""));
|
|
966
|
+
/**
|
|
967
|
+
* base32 encoding from RFC 4648. No padding.
|
|
968
|
+
* Use `base32` for padded version.
|
|
969
|
+
* Also check out `base32hex`, `base32hexnopad`, `base32crockford`.
|
|
970
|
+
* @example
|
|
971
|
+
* ```js
|
|
972
|
+
* base32nopad.encode(Uint8Array.from([0x12, 0xab]));
|
|
973
|
+
* // => 'CKVQ'
|
|
974
|
+
* base32nopad.decode('CKVQ');
|
|
975
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
978
|
+
const base32nopad = chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), join(""));
|
|
979
|
+
/**
|
|
980
|
+
* base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet.
|
|
981
|
+
* Use `base32hexnopad` for unpadded version.
|
|
982
|
+
* @example
|
|
983
|
+
* ```js
|
|
984
|
+
* base32hex.encode(Uint8Array.from([0x12, 0xab]));
|
|
985
|
+
* // => '2ALG===='
|
|
986
|
+
* base32hex.decode('2ALG====');
|
|
987
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
988
|
+
* ```
|
|
989
|
+
*/
|
|
990
|
+
const base32hex = chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), padding(5), join(""));
|
|
991
|
+
/**
|
|
992
|
+
* base32 encoding from RFC 4648. No padding. Compared to ordinary `base32`, slightly different alphabet.
|
|
993
|
+
* Use `base32hex` for padded version.
|
|
994
|
+
* @example
|
|
995
|
+
* ```js
|
|
996
|
+
* base32hexnopad.encode(Uint8Array.from([0x12, 0xab]));
|
|
997
|
+
* // => '2ALG'
|
|
998
|
+
* base32hexnopad.decode('2ALG');
|
|
999
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
const base32hexnopad = chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), join(""));
|
|
1003
|
+
/**
|
|
1004
|
+
* base32 encoding from RFC 4648. Doug Crockford's version.
|
|
1005
|
+
* https://www.crockford.com/base32.html
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```js
|
|
1008
|
+
* base32crockford.encode(Uint8Array.from([0x12, 0xab]));
|
|
1009
|
+
* // => '2ANG'
|
|
1010
|
+
* base32crockford.decode('2ANG');
|
|
1011
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1012
|
+
* ```
|
|
1013
|
+
*/
|
|
1014
|
+
const base32crockford = chain(radix2(5), alphabet("0123456789ABCDEFGHJKMNPQRSTVWXYZ"), join(""), normalize((s) => s.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1")));
|
|
1015
|
+
const hasBase64Builtin = typeof Uint8Array.from([]).toBase64 === "function" && typeof Uint8Array.fromBase64 === "function";
|
|
1016
|
+
const decodeBase64Builtin = (s, isUrl) => {
|
|
1017
|
+
astr("base64", s);
|
|
1018
|
+
const re = isUrl ? /^[A-Za-z0-9=_-]+$/ : /^[A-Za-z0-9=+/]+$/;
|
|
1019
|
+
const alphabet = isUrl ? "base64url" : "base64";
|
|
1020
|
+
if (s.length > 0 && !re.test(s)) throw new Error("invalid base64");
|
|
1021
|
+
return Uint8Array.fromBase64(s, {
|
|
1022
|
+
alphabet,
|
|
1023
|
+
lastChunkHandling: "strict"
|
|
1024
|
+
});
|
|
1025
|
+
};
|
|
1026
|
+
/**
|
|
1027
|
+
* base64 from RFC 4648. Padded.
|
|
1028
|
+
* Use `base64nopad` for unpadded version.
|
|
1029
|
+
* Also check out `base64url`, `base64urlnopad`.
|
|
1030
|
+
* Falls back to built-in function, when available.
|
|
1031
|
+
* @example
|
|
1032
|
+
* ```js
|
|
1033
|
+
* base64.encode(Uint8Array.from([0x12, 0xab]));
|
|
1034
|
+
* // => 'Eqs='
|
|
1035
|
+
* base64.decode('Eqs=');
|
|
1036
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
const base64 = hasBase64Builtin ? {
|
|
1040
|
+
encode(b) {
|
|
1041
|
+
abytes(b);
|
|
1042
|
+
return b.toBase64();
|
|
1043
|
+
},
|
|
1044
|
+
decode(s) {
|
|
1045
|
+
return decodeBase64Builtin(s, false);
|
|
1046
|
+
}
|
|
1047
|
+
} : chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), padding(6), join(""));
|
|
1048
|
+
/**
|
|
1049
|
+
* base64 from RFC 4648. No padding.
|
|
1050
|
+
* Use `base64` for padded version.
|
|
1051
|
+
* @example
|
|
1052
|
+
* ```js
|
|
1053
|
+
* base64nopad.encode(Uint8Array.from([0x12, 0xab]));
|
|
1054
|
+
* // => 'Eqs'
|
|
1055
|
+
* base64nopad.decode('Eqs');
|
|
1056
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
const base64nopad = chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), join(""));
|
|
1060
|
+
/**
|
|
1061
|
+
* base64 from RFC 4648, using URL-safe alphabet. Padded.
|
|
1062
|
+
* Use `base64urlnopad` for unpadded version.
|
|
1063
|
+
* Falls back to built-in function, when available.
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```js
|
|
1066
|
+
* base64url.encode(Uint8Array.from([0x12, 0xab]));
|
|
1067
|
+
* // => 'Eqs='
|
|
1068
|
+
* base64url.decode('Eqs=');
|
|
1069
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
1072
|
+
const base64url = hasBase64Builtin ? {
|
|
1073
|
+
encode(b) {
|
|
1074
|
+
abytes(b);
|
|
1075
|
+
return b.toBase64({ alphabet: "base64url" });
|
|
1076
|
+
},
|
|
1077
|
+
decode(s) {
|
|
1078
|
+
return decodeBase64Builtin(s, true);
|
|
1079
|
+
}
|
|
1080
|
+
} : chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), padding(6), join(""));
|
|
1081
|
+
/**
|
|
1082
|
+
* base64 from RFC 4648, using URL-safe alphabet. No padding.
|
|
1083
|
+
* Use `base64url` for padded version.
|
|
1084
|
+
* @example
|
|
1085
|
+
* ```js
|
|
1086
|
+
* base64urlnopad.encode(Uint8Array.from([0x12, 0xab]));
|
|
1087
|
+
* // => 'Eqs'
|
|
1088
|
+
* base64urlnopad.decode('Eqs');
|
|
1089
|
+
* // => Uint8Array.from([0x12, 0xab])
|
|
1090
|
+
* ```
|
|
1091
|
+
*/
|
|
1092
|
+
const base64urlnopad = chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), join(""));
|
|
1093
|
+
const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc) => chain(radix(58), alphabet(abc), join(""));
|
|
1094
|
+
/**
|
|
1095
|
+
* base58: base64 without ambigous characters +, /, 0, O, I, l.
|
|
1096
|
+
* Quadratic (O(n^2)) - so, can't be used on large inputs.
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```js
|
|
1099
|
+
* base58.decode('01abcdef');
|
|
1100
|
+
* // => '3UhJW'
|
|
1101
|
+
* ```
|
|
1102
|
+
*/
|
|
1103
|
+
const base58 = /* @__PURE__ */ genBase58("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz");
|
|
1104
|
+
const BECH_ALPHABET = chain(alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), join(""));
|
|
1105
|
+
const POLYMOD_GENERATORS = [
|
|
1106
|
+
996825010,
|
|
1107
|
+
642813549,
|
|
1108
|
+
513874426,
|
|
1109
|
+
1027748829,
|
|
1110
|
+
705979059
|
|
1111
|
+
];
|
|
1112
|
+
function bech32Polymod(pre) {
|
|
1113
|
+
const b = pre >> 25;
|
|
1114
|
+
let chk = (pre & 33554431) << 5;
|
|
1115
|
+
for (let i = 0; i < POLYMOD_GENERATORS.length; i++) if ((b >> i & 1) === 1) chk ^= POLYMOD_GENERATORS[i];
|
|
1116
|
+
return chk;
|
|
1117
|
+
}
|
|
1118
|
+
function bechChecksum(prefix, words, encodingConst = 1) {
|
|
1119
|
+
const len = prefix.length;
|
|
1120
|
+
let chk = 1;
|
|
1121
|
+
for (let i = 0; i < len; i++) {
|
|
1122
|
+
const c = prefix.charCodeAt(i);
|
|
1123
|
+
if (c < 33 || c > 126) throw new Error(`Invalid prefix (${prefix})`);
|
|
1124
|
+
chk = bech32Polymod(chk) ^ c >> 5;
|
|
1125
|
+
}
|
|
1126
|
+
chk = bech32Polymod(chk);
|
|
1127
|
+
for (let i = 0; i < len; i++) chk = bech32Polymod(chk) ^ prefix.charCodeAt(i) & 31;
|
|
1128
|
+
for (let v of words) chk = bech32Polymod(chk) ^ v;
|
|
1129
|
+
for (let i = 0; i < 6; i++) chk = bech32Polymod(chk);
|
|
1130
|
+
chk ^= encodingConst;
|
|
1131
|
+
return BECH_ALPHABET.encode(convertRadix2([chk % powers[30]], 30, 5, false));
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* @__NO_SIDE_EFFECTS__
|
|
1135
|
+
*/
|
|
1136
|
+
function genBech32(encoding) {
|
|
1137
|
+
const ENCODING_CONST = encoding === "bech32" ? 1 : 734539939;
|
|
1138
|
+
const _words = radix2(5);
|
|
1139
|
+
const fromWords = _words.decode;
|
|
1140
|
+
const toWords = _words.encode;
|
|
1141
|
+
const fromWordsUnsafe = unsafeWrapper(fromWords);
|
|
1142
|
+
function encode(prefix, words, limit = 90) {
|
|
1143
|
+
astr("bech32.encode prefix", prefix);
|
|
1144
|
+
if (isBytes(words)) words = Array.from(words);
|
|
1145
|
+
anumArr("bech32.encode", words);
|
|
1146
|
+
const plen = prefix.length;
|
|
1147
|
+
if (plen === 0) throw new TypeError(`Invalid prefix length ${plen}`);
|
|
1148
|
+
const actualLength = plen + 7 + words.length;
|
|
1149
|
+
if (limit !== false && actualLength > limit) throw new TypeError(`Length ${actualLength} exceeds limit ${limit}`);
|
|
1150
|
+
const lowered = prefix.toLowerCase();
|
|
1151
|
+
const sum = bechChecksum(lowered, words, ENCODING_CONST);
|
|
1152
|
+
return `${lowered}1${BECH_ALPHABET.encode(words)}${sum}`;
|
|
1153
|
+
}
|
|
1154
|
+
function decode(str, limit = 90) {
|
|
1155
|
+
astr("bech32.decode input", str);
|
|
1156
|
+
const slen = str.length;
|
|
1157
|
+
if (slen < 8 || limit !== false && slen > limit) throw new TypeError(`invalid string length: ${slen} (${str}). Expected (8..${limit})`);
|
|
1158
|
+
const lowered = str.toLowerCase();
|
|
1159
|
+
if (str !== lowered && str !== str.toUpperCase()) throw new Error(`String must be lowercase or uppercase`);
|
|
1160
|
+
const sepIndex = lowered.lastIndexOf("1");
|
|
1161
|
+
if (sepIndex === 0 || sepIndex === -1) throw new Error(`Letter "1" must be present between prefix and data only`);
|
|
1162
|
+
const prefix = lowered.slice(0, sepIndex);
|
|
1163
|
+
const data = lowered.slice(sepIndex + 1);
|
|
1164
|
+
if (data.length < 6) throw new Error("Data must be at least 6 characters long");
|
|
1165
|
+
const words = BECH_ALPHABET.decode(data).slice(0, -6);
|
|
1166
|
+
const sum = bechChecksum(prefix, words, ENCODING_CONST);
|
|
1167
|
+
if (!data.endsWith(sum)) throw new Error(`Invalid checksum in ${str}: expected "${sum}"`);
|
|
1168
|
+
return {
|
|
1169
|
+
prefix,
|
|
1170
|
+
words
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const decodeUnsafe = unsafeWrapper(decode);
|
|
1174
|
+
function decodeToBytes(str) {
|
|
1175
|
+
const { prefix, words } = decode(str, false);
|
|
1176
|
+
return {
|
|
1177
|
+
prefix,
|
|
1178
|
+
words,
|
|
1179
|
+
bytes: fromWords(words)
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function encodeFromBytes(prefix, bytes) {
|
|
1183
|
+
return encode(prefix, toWords(bytes));
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
encode,
|
|
1187
|
+
decode,
|
|
1188
|
+
encodeFromBytes,
|
|
1189
|
+
decodeToBytes,
|
|
1190
|
+
decodeUnsafe,
|
|
1191
|
+
fromWords,
|
|
1192
|
+
fromWordsUnsafe,
|
|
1193
|
+
toWords
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* bech32 from BIP 173. Operates on words.
|
|
1198
|
+
* For high-level, check out scure-btc-signer:
|
|
1199
|
+
* https://github.com/paulmillr/scure-btc-signer.
|
|
1200
|
+
*/
|
|
1201
|
+
const bech32 = genBech32("bech32");
|
|
1202
|
+
/**
|
|
1203
|
+
* bech32m from BIP 350. Operates on words.
|
|
1204
|
+
* It was to mitigate `bech32` weaknesses.
|
|
1205
|
+
* For high-level, check out scure-btc-signer:
|
|
1206
|
+
* https://github.com/paulmillr/scure-btc-signer.
|
|
1207
|
+
*/
|
|
1208
|
+
const bech32m = genBech32("bech32m");
|
|
1209
|
+
const hasHexBuiltin = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
|
|
1210
|
+
const hexBuiltin = {
|
|
1211
|
+
encode(data) {
|
|
1212
|
+
abytes(data);
|
|
1213
|
+
return data.toHex();
|
|
1214
|
+
},
|
|
1215
|
+
decode(s) {
|
|
1216
|
+
astr("hex", s);
|
|
1217
|
+
return Uint8Array.fromHex(s);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
/**
|
|
1221
|
+
* hex string decoder. Uses built-in function, when available.
|
|
1222
|
+
* @example
|
|
1223
|
+
* ```js
|
|
1224
|
+
* const b = hex.decode("0102ff"); // => new Uint8Array([ 1, 2, 255 ])
|
|
1225
|
+
* const str = hex.encode(b); // "0102ff"
|
|
1226
|
+
* ```
|
|
1227
|
+
*/
|
|
1228
|
+
const hex = hasHexBuiltin ? hexBuiltin : chain(radix2(4), alphabet("0123456789abcdef"), join(""), normalize((s) => {
|
|
1229
|
+
if (typeof s !== "string" || s.length % 2 !== 0) throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`);
|
|
1230
|
+
return s.toLowerCase();
|
|
1231
|
+
}));
|
|
1232
|
+
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/crypto/base58.ts
|
|
1235
|
+
/**
|
|
1236
|
+
* Base58 decoder wrapper
|
|
1237
|
+
* Uses @scure/base for audited, tree-shakeable implementation
|
|
1238
|
+
*/
|
|
1239
|
+
/**
|
|
1240
|
+
* Decode a Base58-encoded string to bytes
|
|
1241
|
+
*
|
|
1242
|
+
* Preserves leading zero bytes (represented as leading '1' characters)
|
|
1243
|
+
*
|
|
1244
|
+
* @param input - Base58 string
|
|
1245
|
+
* @returns Decoded bytes
|
|
1246
|
+
* @throws Error if input contains invalid Base58 characters
|
|
1247
|
+
*/
|
|
1248
|
+
function decodeBase58(input) {
|
|
1249
|
+
try {
|
|
1250
|
+
return base58.decode(input);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1253
|
+
throw new Error(`Invalid Base58: ${message}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/validation/solana-address.ts
|
|
1259
|
+
/**
|
|
1260
|
+
* Solana address validation (Base58 + 32-byte length)
|
|
1261
|
+
*/
|
|
1262
|
+
const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
1263
|
+
/**
|
|
1264
|
+
* Validate a Solana address (Base58 encoded public key)
|
|
1265
|
+
*
|
|
1266
|
+
* Checks Base58 format and verifies decoded length is exactly 32 bytes
|
|
1267
|
+
*
|
|
1268
|
+
* @param address - Address to validate
|
|
1269
|
+
* @param field - Field path for error reporting
|
|
1270
|
+
* @returns Array of validation issues (empty if valid)
|
|
1271
|
+
*/
|
|
1272
|
+
function validateSolanaAddress(address, field) {
|
|
1273
|
+
if (!SOLANA_ADDRESS_REGEX.test(address)) return [{
|
|
1274
|
+
code: ErrorCode.INVALID_SOLANA_ADDRESS,
|
|
1275
|
+
field,
|
|
1276
|
+
message: "Solana address must be 32-44 Base58 characters",
|
|
1277
|
+
severity: "error",
|
|
1278
|
+
fix: "Valid characters: 1-9, A-H, J-N, P-Z, a-k, m-z (no 0, O, I, l)"
|
|
1279
|
+
}];
|
|
1280
|
+
try {
|
|
1281
|
+
const decoded = decodeBase58(address);
|
|
1282
|
+
if (decoded.length !== 32) return [{
|
|
1283
|
+
code: ErrorCode.INVALID_SOLANA_ADDRESS,
|
|
1284
|
+
field,
|
|
1285
|
+
message: `Solana address must decode to 32 bytes, got ${decoded.length}`,
|
|
1286
|
+
severity: "error",
|
|
1287
|
+
fix: "Verify address is a valid Solana public key"
|
|
1288
|
+
}];
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
return [{
|
|
1291
|
+
code: ErrorCode.INVALID_SOLANA_ADDRESS,
|
|
1292
|
+
field,
|
|
1293
|
+
message: "Invalid Base58 encoding",
|
|
1294
|
+
severity: "error",
|
|
1295
|
+
fix: error instanceof Error ? error.message : "Check Base58 encoding"
|
|
1296
|
+
}];
|
|
1297
|
+
}
|
|
1298
|
+
return [];
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/validation/address.ts
|
|
1303
|
+
/**
|
|
1304
|
+
* Address validation with CAIP-2 namespace dispatch
|
|
1305
|
+
*
|
|
1306
|
+
* Dispatches to chain-specific validators based on network namespace
|
|
1307
|
+
*/
|
|
1308
|
+
/**
|
|
1309
|
+
* Validate an address for a specific network
|
|
1310
|
+
*
|
|
1311
|
+
* Dispatches to appropriate chain-specific validator based on CAIP-2 namespace:
|
|
1312
|
+
* - eip155:* → EVM address validation
|
|
1313
|
+
* - solana:* → Solana address validation
|
|
1314
|
+
* - stellar:*, aptos:* → Accept any string (deep validation deferred)
|
|
1315
|
+
* - Unknown namespaces → Accept any string (registry warnings handled elsewhere)
|
|
1316
|
+
*
|
|
1317
|
+
* Cross-chain mismatches are caught naturally by dispatch:
|
|
1318
|
+
* - EVM address (0x...) on Solana network → fails Solana Base58 validation
|
|
1319
|
+
* - Solana address on EVM network → fails EVM 0x-prefix validation
|
|
1320
|
+
*
|
|
1321
|
+
* @param address - Address to validate
|
|
1322
|
+
* @param network - CAIP-2 network identifier
|
|
1323
|
+
* @param field - Field path for error reporting
|
|
1324
|
+
* @returns Array of validation issues (empty if valid)
|
|
1325
|
+
*/
|
|
1326
|
+
function validateAddress(address, network, field) {
|
|
1327
|
+
const namespace = getNetworkNamespace(network);
|
|
1328
|
+
if (namespace === void 0) return [];
|
|
1329
|
+
switch (namespace) {
|
|
1330
|
+
case "eip155": return validateEvmAddress(address, field);
|
|
1331
|
+
case "solana": return validateSolanaAddress(address, field);
|
|
1332
|
+
case "stellar":
|
|
1333
|
+
case "aptos": return [];
|
|
1334
|
+
default: return [];
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
//#endregion
|
|
1339
|
+
//#region src/validation/rules/structure.ts
|
|
1340
|
+
/**
|
|
1341
|
+
* Validate input structure: parse JSON, check object, detect format.
|
|
1342
|
+
*
|
|
1343
|
+
* @param input - Raw JSON string or object
|
|
1344
|
+
* @returns StructureResult with parsed object, detected format, and issues
|
|
1345
|
+
*/
|
|
1346
|
+
function validateStructure(input) {
|
|
1347
|
+
const issues = [];
|
|
1348
|
+
const { parsed, error } = parseInput(input);
|
|
1349
|
+
if (error) return {
|
|
1350
|
+
parsed: null,
|
|
1351
|
+
format: "unknown",
|
|
1352
|
+
issues: [error]
|
|
1353
|
+
};
|
|
1354
|
+
if (!isRecord(parsed)) {
|
|
1355
|
+
issues.push({
|
|
1356
|
+
code: ErrorCode.NOT_OBJECT,
|
|
1357
|
+
field: "$",
|
|
1358
|
+
message: ErrorMessages.NOT_OBJECT,
|
|
1359
|
+
severity: "error"
|
|
1360
|
+
});
|
|
1361
|
+
return {
|
|
1362
|
+
parsed: null,
|
|
1363
|
+
format: "unknown",
|
|
1364
|
+
issues
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const format = detect(parsed);
|
|
1368
|
+
if (format === "unknown") issues.push({
|
|
1369
|
+
code: ErrorCode.UNKNOWN_FORMAT,
|
|
1370
|
+
field: "$",
|
|
1371
|
+
message: ErrorMessages.UNKNOWN_FORMAT,
|
|
1372
|
+
severity: "error"
|
|
1373
|
+
});
|
|
1374
|
+
return {
|
|
1375
|
+
parsed,
|
|
1376
|
+
format,
|
|
1377
|
+
issues
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
//#endregion
|
|
1382
|
+
//#region src/validation/rules/version.ts
|
|
1383
|
+
/**
|
|
1384
|
+
* Validate x402Version field.
|
|
1385
|
+
*
|
|
1386
|
+
* Since normalize() always sets x402Version: 2, this mainly validates
|
|
1387
|
+
* the original format's version field. If somehow the value isn't 1 or 2,
|
|
1388
|
+
* push INVALID_VERSION error.
|
|
1389
|
+
*
|
|
1390
|
+
* Note: No MISSING_VERSION check needed here because normalize() always
|
|
1391
|
+
* sets it. The orchestrator handles version-related warnings for legacy
|
|
1392
|
+
* formats via the legacy rule module.
|
|
1393
|
+
*
|
|
1394
|
+
* @param config - Normalized config
|
|
1395
|
+
* @param _detectedFormat - Detected format (reserved for future use)
|
|
1396
|
+
* @returns Array of validation issues
|
|
1397
|
+
*/
|
|
1398
|
+
function validateVersion(config, _detectedFormat) {
|
|
1399
|
+
const issues = [];
|
|
1400
|
+
const version = config.x402Version;
|
|
1401
|
+
if (version !== 1 && version !== 2) issues.push({
|
|
1402
|
+
code: ErrorCode.INVALID_VERSION,
|
|
1403
|
+
field: "x402Version",
|
|
1404
|
+
message: ErrorMessages.INVALID_VERSION,
|
|
1405
|
+
severity: "error"
|
|
1406
|
+
});
|
|
1407
|
+
return issues;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region src/validation/rules/fields.ts
|
|
1412
|
+
/**
|
|
1413
|
+
* Validate required fields on a single accepts entry.
|
|
1414
|
+
*
|
|
1415
|
+
* @param entry - Accepts entry to validate
|
|
1416
|
+
* @param fieldPath - Dot-notation path for issue reporting (e.g. "accepts[0]")
|
|
1417
|
+
* @returns Array of validation issues
|
|
1418
|
+
*/
|
|
1419
|
+
function validateFields(entry, fieldPath) {
|
|
1420
|
+
const issues = [];
|
|
1421
|
+
if (!entry.scheme) issues.push({
|
|
1422
|
+
code: ErrorCode.MISSING_SCHEME,
|
|
1423
|
+
field: `${fieldPath}.scheme`,
|
|
1424
|
+
message: ErrorMessages.MISSING_SCHEME,
|
|
1425
|
+
severity: "error"
|
|
1426
|
+
});
|
|
1427
|
+
if (!entry.network) issues.push({
|
|
1428
|
+
code: ErrorCode.MISSING_NETWORK,
|
|
1429
|
+
field: `${fieldPath}.network`,
|
|
1430
|
+
message: ErrorMessages.MISSING_NETWORK,
|
|
1431
|
+
severity: "error"
|
|
1432
|
+
});
|
|
1433
|
+
if (!entry.amount) issues.push({
|
|
1434
|
+
code: ErrorCode.MISSING_AMOUNT,
|
|
1435
|
+
field: `${fieldPath}.amount`,
|
|
1436
|
+
message: ErrorMessages.MISSING_AMOUNT,
|
|
1437
|
+
severity: "error"
|
|
1438
|
+
});
|
|
1439
|
+
if (!entry.asset) issues.push({
|
|
1440
|
+
code: ErrorCode.MISSING_ASSET,
|
|
1441
|
+
field: `${fieldPath}.asset`,
|
|
1442
|
+
message: ErrorMessages.MISSING_ASSET,
|
|
1443
|
+
severity: "error"
|
|
1444
|
+
});
|
|
1445
|
+
if (!entry.payTo) issues.push({
|
|
1446
|
+
code: ErrorCode.MISSING_PAY_TO,
|
|
1447
|
+
field: `${fieldPath}.payTo`,
|
|
1448
|
+
message: ErrorMessages.MISSING_PAY_TO,
|
|
1449
|
+
severity: "error"
|
|
1450
|
+
});
|
|
1451
|
+
return issues;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Validate the accepts array itself (presence, type, emptiness).
|
|
1455
|
+
*
|
|
1456
|
+
* @param config - Normalized config
|
|
1457
|
+
* @returns Array of validation issues
|
|
1458
|
+
*/
|
|
1459
|
+
function validateAccepts(config) {
|
|
1460
|
+
const issues = [];
|
|
1461
|
+
if (!Array.isArray(config.accepts)) {
|
|
1462
|
+
issues.push({
|
|
1463
|
+
code: ErrorCode.INVALID_ACCEPTS,
|
|
1464
|
+
field: "accepts",
|
|
1465
|
+
message: ErrorMessages.INVALID_ACCEPTS,
|
|
1466
|
+
severity: "error"
|
|
1467
|
+
});
|
|
1468
|
+
return issues;
|
|
1469
|
+
}
|
|
1470
|
+
if (config.accepts.length === 0) issues.push({
|
|
1471
|
+
code: ErrorCode.EMPTY_ACCEPTS,
|
|
1472
|
+
field: "accepts",
|
|
1473
|
+
message: ErrorMessages.EMPTY_ACCEPTS,
|
|
1474
|
+
severity: "error"
|
|
1475
|
+
});
|
|
1476
|
+
return issues;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Validate resource object on normalized config.
|
|
1480
|
+
*
|
|
1481
|
+
* For v2 configs, resource is expected. Its absence is a warning, not an error,
|
|
1482
|
+
* since some v2 configs work without it.
|
|
1483
|
+
*
|
|
1484
|
+
* Also validates URL format via new URL() constructor (RULE-04).
|
|
1485
|
+
*
|
|
1486
|
+
* @param config - Normalized config
|
|
1487
|
+
* @param detectedFormat - Detected format
|
|
1488
|
+
* @returns Array of validation issues
|
|
1489
|
+
*/
|
|
1490
|
+
function validateResource(config, detectedFormat) {
|
|
1491
|
+
const issues = [];
|
|
1492
|
+
if (!config.resource) {
|
|
1493
|
+
if (detectedFormat === "v2") issues.push({
|
|
1494
|
+
code: ErrorCode.MISSING_RESOURCE,
|
|
1495
|
+
field: "resource",
|
|
1496
|
+
message: ErrorMessages.MISSING_RESOURCE,
|
|
1497
|
+
severity: "warning"
|
|
1498
|
+
});
|
|
1499
|
+
return issues;
|
|
1500
|
+
}
|
|
1501
|
+
if (!config.resource.url) {
|
|
1502
|
+
issues.push({
|
|
1503
|
+
code: ErrorCode.MISSING_RESOURCE,
|
|
1504
|
+
field: "resource.url",
|
|
1505
|
+
message: ErrorMessages.MISSING_RESOURCE,
|
|
1506
|
+
severity: "warning"
|
|
1507
|
+
});
|
|
1508
|
+
return issues;
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
new URL(config.resource.url);
|
|
1512
|
+
} catch {
|
|
1513
|
+
issues.push({
|
|
1514
|
+
code: ErrorCode.INVALID_URL,
|
|
1515
|
+
field: "resource.url",
|
|
1516
|
+
message: "resource.url is not a valid URL format",
|
|
1517
|
+
severity: "warning"
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
return issues;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/registries/simple-names.ts
|
|
1525
|
+
const SIMPLE_NAME_TO_CAIP2 = {
|
|
1526
|
+
base: "eip155:8453",
|
|
1527
|
+
"base-sepolia": "eip155:84532",
|
|
1528
|
+
base_sepolia: "eip155:84532",
|
|
1529
|
+
avalanche: "eip155:43114",
|
|
1530
|
+
"avalanche-fuji": "eip155:43113",
|
|
1531
|
+
solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
1532
|
+
"solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
1533
|
+
"solana-testnet": "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",
|
|
1534
|
+
stellar: "stellar:pubnet",
|
|
1535
|
+
"stellar-testnet": "stellar:testnet",
|
|
1536
|
+
aptos: "aptos:1"
|
|
1537
|
+
};
|
|
1538
|
+
function getCanonicalNetwork(name) {
|
|
1539
|
+
return SIMPLE_NAME_TO_CAIP2[name.toLowerCase()];
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/registries/assets.ts
|
|
1544
|
+
const KNOWN_ASSETS = {
|
|
1545
|
+
"eip155:8453": { "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": {
|
|
1546
|
+
symbol: "USDC",
|
|
1547
|
+
name: "USD Coin",
|
|
1548
|
+
decimals: 6
|
|
1549
|
+
} },
|
|
1550
|
+
"eip155:84532": { "0x036cbd53842c5426634e7929541ec2318f3dcf7e": {
|
|
1551
|
+
symbol: "USDC",
|
|
1552
|
+
name: "USD Coin",
|
|
1553
|
+
decimals: 6
|
|
1554
|
+
} },
|
|
1555
|
+
"eip155:43114": { "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e": {
|
|
1556
|
+
symbol: "USDC",
|
|
1557
|
+
name: "USD Coin",
|
|
1558
|
+
decimals: 6
|
|
1559
|
+
} },
|
|
1560
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: {
|
|
1561
|
+
symbol: "USDC",
|
|
1562
|
+
name: "USD Coin",
|
|
1563
|
+
decimals: 6
|
|
1564
|
+
} }
|
|
1565
|
+
};
|
|
1566
|
+
function isKnownAsset(network, address) {
|
|
1567
|
+
const networkAssets = KNOWN_ASSETS[network];
|
|
1568
|
+
if (!networkAssets) return false;
|
|
1569
|
+
return (getNetworkNamespace(network) === "eip155" ? address.toLowerCase() : address) in networkAssets;
|
|
1570
|
+
}
|
|
1571
|
+
function getAssetInfo(network, address) {
|
|
1572
|
+
const networkAssets = KNOWN_ASSETS[network];
|
|
1573
|
+
if (!networkAssets) return;
|
|
1574
|
+
return networkAssets[getNetworkNamespace(network) === "eip155" ? address.toLowerCase() : address];
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/validation/rules/network.ts
|
|
1579
|
+
/**
|
|
1580
|
+
* Validate network field on a single accepts entry.
|
|
1581
|
+
*
|
|
1582
|
+
* Checks CAIP-2 format and known network registry. Provides fix
|
|
1583
|
+
* suggestions for simple chain names that have canonical CAIP-2 mappings.
|
|
1584
|
+
*
|
|
1585
|
+
* @param entry - Accepts entry to validate
|
|
1586
|
+
* @param fieldPath - Dot-notation path for issue reporting (e.g. "accepts[0]")
|
|
1587
|
+
* @returns Array of validation issues
|
|
1588
|
+
*/
|
|
1589
|
+
function validateNetwork(entry, fieldPath) {
|
|
1590
|
+
const issues = [];
|
|
1591
|
+
if (!entry.network) return issues;
|
|
1592
|
+
if (!isValidCaip2(entry.network)) {
|
|
1593
|
+
const canonical = getCanonicalNetwork(entry.network);
|
|
1594
|
+
if (canonical) issues.push({
|
|
1595
|
+
code: ErrorCode.INVALID_NETWORK_FORMAT,
|
|
1596
|
+
field: `${fieldPath}.network`,
|
|
1597
|
+
message: ErrorMessages.INVALID_NETWORK_FORMAT,
|
|
1598
|
+
severity: "error",
|
|
1599
|
+
fix: `Use '${canonical}' instead of '${entry.network}'`
|
|
1600
|
+
});
|
|
1601
|
+
else issues.push({
|
|
1602
|
+
code: ErrorCode.INVALID_NETWORK_FORMAT,
|
|
1603
|
+
field: `${fieldPath}.network`,
|
|
1604
|
+
message: ErrorMessages.INVALID_NETWORK_FORMAT,
|
|
1605
|
+
severity: "error"
|
|
1606
|
+
});
|
|
1607
|
+
return issues;
|
|
1608
|
+
}
|
|
1609
|
+
if (!isKnownNetwork(entry.network)) issues.push({
|
|
1610
|
+
code: ErrorCode.UNKNOWN_NETWORK,
|
|
1611
|
+
field: `${fieldPath}.network`,
|
|
1612
|
+
message: ErrorMessages.UNKNOWN_NETWORK,
|
|
1613
|
+
severity: "warning"
|
|
1614
|
+
});
|
|
1615
|
+
return issues;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Validate asset field on a single accepts entry.
|
|
1619
|
+
*
|
|
1620
|
+
* Checks if the asset is known for the given network. Only checks
|
|
1621
|
+
* when both network and asset are present and network is valid.
|
|
1622
|
+
*
|
|
1623
|
+
* @param entry - Accepts entry to validate
|
|
1624
|
+
* @param fieldPath - Dot-notation path for issue reporting (e.g. "accepts[0]")
|
|
1625
|
+
* @returns Array of validation issues
|
|
1626
|
+
*/
|
|
1627
|
+
function validateAsset(entry, fieldPath) {
|
|
1628
|
+
const issues = [];
|
|
1629
|
+
if (!entry.asset) return issues;
|
|
1630
|
+
if (entry.network && isValidCaip2(entry.network) && !isKnownAsset(entry.network, entry.asset)) issues.push({
|
|
1631
|
+
code: ErrorCode.UNKNOWN_ASSET,
|
|
1632
|
+
field: `${fieldPath}.asset`,
|
|
1633
|
+
message: ErrorMessages.UNKNOWN_ASSET,
|
|
1634
|
+
severity: "warning"
|
|
1635
|
+
});
|
|
1636
|
+
return issues;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
//#endregion
|
|
1640
|
+
//#region src/validation/rules/amount.ts
|
|
1641
|
+
/**
|
|
1642
|
+
* Validate amount field on a single accepts entry.
|
|
1643
|
+
*
|
|
1644
|
+
* Amount must be a digit-only string (no decimals, signs, or scientific notation)
|
|
1645
|
+
* and must be greater than zero.
|
|
1646
|
+
*
|
|
1647
|
+
* @param entry - Accepts entry to validate
|
|
1648
|
+
* @param fieldPath - Dot-notation path for issue reporting (e.g. "accepts[0]")
|
|
1649
|
+
* @returns Array of validation issues
|
|
1650
|
+
*/
|
|
1651
|
+
function validateAmount(entry, fieldPath) {
|
|
1652
|
+
const issues = [];
|
|
1653
|
+
if (!entry.amount) return issues;
|
|
1654
|
+
if (!/^\d+$/.test(entry.amount)) {
|
|
1655
|
+
issues.push({
|
|
1656
|
+
code: ErrorCode.INVALID_AMOUNT,
|
|
1657
|
+
field: `${fieldPath}.amount`,
|
|
1658
|
+
message: ErrorMessages.INVALID_AMOUNT,
|
|
1659
|
+
severity: "error"
|
|
1660
|
+
});
|
|
1661
|
+
return issues;
|
|
1662
|
+
}
|
|
1663
|
+
if (entry.amount === "0") issues.push({
|
|
1664
|
+
code: ErrorCode.ZERO_AMOUNT,
|
|
1665
|
+
field: `${fieldPath}.amount`,
|
|
1666
|
+
message: ErrorMessages.ZERO_AMOUNT,
|
|
1667
|
+
severity: "error"
|
|
1668
|
+
});
|
|
1669
|
+
return issues;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Validate maxTimeoutSeconds on a single accepts entry.
|
|
1673
|
+
*
|
|
1674
|
+
* For v2 format, missing timeout produces a warning.
|
|
1675
|
+
* When present, timeout must be a positive integer (RULE-10).
|
|
1676
|
+
*
|
|
1677
|
+
* @param entry - Accepts entry to validate
|
|
1678
|
+
* @param fieldPath - Dot-notation path for issue reporting (e.g. "accepts[0]")
|
|
1679
|
+
* @param detectedFormat - Detected config format
|
|
1680
|
+
* @returns Array of validation issues
|
|
1681
|
+
*/
|
|
1682
|
+
function validateTimeout(entry, fieldPath, detectedFormat) {
|
|
1683
|
+
const issues = [];
|
|
1684
|
+
if (entry.maxTimeoutSeconds === void 0) {
|
|
1685
|
+
if (detectedFormat === "v2") issues.push({
|
|
1686
|
+
code: ErrorCode.MISSING_MAX_TIMEOUT,
|
|
1687
|
+
field: `${fieldPath}.maxTimeoutSeconds`,
|
|
1688
|
+
message: ErrorMessages.MISSING_MAX_TIMEOUT,
|
|
1689
|
+
severity: "warning"
|
|
1690
|
+
});
|
|
1691
|
+
return issues;
|
|
1692
|
+
}
|
|
1693
|
+
if (typeof entry.maxTimeoutSeconds !== "number") {
|
|
1694
|
+
issues.push({
|
|
1695
|
+
code: ErrorCode.INVALID_TIMEOUT,
|
|
1696
|
+
field: `${fieldPath}.maxTimeoutSeconds`,
|
|
1697
|
+
message: ErrorMessages.INVALID_TIMEOUT,
|
|
1698
|
+
severity: "error"
|
|
1699
|
+
});
|
|
1700
|
+
return issues;
|
|
1701
|
+
}
|
|
1702
|
+
if (!Number.isInteger(entry.maxTimeoutSeconds)) {
|
|
1703
|
+
issues.push({
|
|
1704
|
+
code: ErrorCode.INVALID_TIMEOUT,
|
|
1705
|
+
field: `${fieldPath}.maxTimeoutSeconds`,
|
|
1706
|
+
message: ErrorMessages.INVALID_TIMEOUT,
|
|
1707
|
+
severity: "error"
|
|
1708
|
+
});
|
|
1709
|
+
return issues;
|
|
1710
|
+
}
|
|
1711
|
+
if (entry.maxTimeoutSeconds <= 0) issues.push({
|
|
1712
|
+
code: ErrorCode.INVALID_TIMEOUT,
|
|
1713
|
+
field: `${fieldPath}.maxTimeoutSeconds`,
|
|
1714
|
+
message: ErrorMessages.INVALID_TIMEOUT,
|
|
1715
|
+
severity: "error"
|
|
1716
|
+
});
|
|
1717
|
+
return issues;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/validation/rules/legacy.ts
|
|
1722
|
+
/**
|
|
1723
|
+
* Validate for legacy format usage and produce upgrade suggestions.
|
|
1724
|
+
*
|
|
1725
|
+
* @param _config - Normalized config (reserved for future use)
|
|
1726
|
+
* @param detectedFormat - Detected config format
|
|
1727
|
+
* @param _originalInput - Original input object (reserved for future use)
|
|
1728
|
+
* @returns Array of validation issues (warnings)
|
|
1729
|
+
*/
|
|
1730
|
+
function validateLegacy(_config, detectedFormat, _originalInput) {
|
|
1731
|
+
const issues = [];
|
|
1732
|
+
if (detectedFormat === "v1") issues.push({
|
|
1733
|
+
code: ErrorCode.LEGACY_FORMAT,
|
|
1734
|
+
field: "$",
|
|
1735
|
+
message: ErrorMessages.LEGACY_FORMAT,
|
|
1736
|
+
severity: "warning",
|
|
1737
|
+
fix: "Upgrade to x402 v2 -- use amount instead of maxAmountRequired, add resource object"
|
|
1738
|
+
});
|
|
1739
|
+
return issues;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
//#endregion
|
|
1743
|
+
//#region src/validation/rules/extensions.ts
|
|
1744
|
+
/**
|
|
1745
|
+
* Check whether a value is a non-null plain object (not an array).
|
|
1746
|
+
*/
|
|
1747
|
+
function isObject(v) {
|
|
1748
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Validate `extensions.bazaar` when present.
|
|
1752
|
+
*
|
|
1753
|
+
* Checks:
|
|
1754
|
+
* - bazaar is an object
|
|
1755
|
+
* - bazaar.info exists and is an object with input (type + method) and output
|
|
1756
|
+
* - bazaar.schema exists and looks like a JSON Schema object
|
|
1757
|
+
*
|
|
1758
|
+
* @returns Array of warning issues (empty when bazaar is absent or valid)
|
|
1759
|
+
*/
|
|
1760
|
+
function validateBazaar(config) {
|
|
1761
|
+
const issues = [];
|
|
1762
|
+
if (!config.extensions) return issues;
|
|
1763
|
+
const bazaar = config.extensions["bazaar"];
|
|
1764
|
+
if (bazaar === void 0) return issues;
|
|
1765
|
+
if (!isObject(bazaar)) {
|
|
1766
|
+
issues.push({
|
|
1767
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1768
|
+
field: "extensions.bazaar",
|
|
1769
|
+
message: "extensions.bazaar must be an object",
|
|
1770
|
+
severity: "warning",
|
|
1771
|
+
fix: "Set extensions.bazaar to an object with info and schema properties"
|
|
1772
|
+
});
|
|
1773
|
+
return issues;
|
|
1774
|
+
}
|
|
1775
|
+
const info = bazaar["info"];
|
|
1776
|
+
if (!isObject(info)) issues.push({
|
|
1777
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1778
|
+
field: "extensions.bazaar.info",
|
|
1779
|
+
message: ErrorMessages.INVALID_BAZAAR_INFO,
|
|
1780
|
+
severity: "warning",
|
|
1781
|
+
fix: "Add an info object with input and output properties describing your API"
|
|
1782
|
+
});
|
|
1783
|
+
else {
|
|
1784
|
+
const input = info["input"];
|
|
1785
|
+
if (!isObject(input) || !input["type"] || !input["method"]) issues.push({
|
|
1786
|
+
code: ErrorCode.INVALID_BAZAAR_INFO_INPUT,
|
|
1787
|
+
field: "extensions.bazaar.info.input",
|
|
1788
|
+
message: ErrorMessages.INVALID_BAZAAR_INFO_INPUT,
|
|
1789
|
+
severity: "warning",
|
|
1790
|
+
fix: "Add input.type (e.g. \"application/json\") and input.method (e.g. \"POST\")"
|
|
1791
|
+
});
|
|
1792
|
+
const output = info["output"];
|
|
1793
|
+
if (!isObject(output)) issues.push({
|
|
1794
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1795
|
+
field: "extensions.bazaar.info.output",
|
|
1796
|
+
message: "extensions.bazaar.info.output must be an object",
|
|
1797
|
+
severity: "warning",
|
|
1798
|
+
fix: "Add an output object describing the API response format"
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
const schema = bazaar["schema"];
|
|
1802
|
+
if (!isObject(schema) || !schema["type"] && !schema["$schema"] && !schema["properties"]) issues.push({
|
|
1803
|
+
code: ErrorCode.INVALID_BAZAAR_SCHEMA,
|
|
1804
|
+
field: "extensions.bazaar.schema",
|
|
1805
|
+
message: ErrorMessages.INVALID_BAZAAR_SCHEMA,
|
|
1806
|
+
severity: "warning",
|
|
1807
|
+
fix: "Add a JSON Schema object with type, $schema, or properties"
|
|
1808
|
+
});
|
|
1809
|
+
return issues;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Validate `accepts[].outputSchema` on the raw parsed input.
|
|
1813
|
+
*
|
|
1814
|
+
* Uses the raw parsed object because AcceptsEntry strips outputSchema during normalization.
|
|
1815
|
+
*
|
|
1816
|
+
* Checks per entry with outputSchema:
|
|
1817
|
+
* - outputSchema is an object
|
|
1818
|
+
* - outputSchema.input exists with type and method
|
|
1819
|
+
* - outputSchema.output exists and is an object
|
|
1820
|
+
*
|
|
1821
|
+
* @returns Array of warning issues
|
|
1822
|
+
*/
|
|
1823
|
+
function validateOutputSchema(parsed) {
|
|
1824
|
+
const issues = [];
|
|
1825
|
+
const accepts = parsed["accepts"];
|
|
1826
|
+
if (!Array.isArray(accepts)) return issues;
|
|
1827
|
+
for (let i = 0; i < accepts.length; i++) {
|
|
1828
|
+
const entry = accepts[i];
|
|
1829
|
+
if (!isObject(entry)) continue;
|
|
1830
|
+
const outputSchema = entry["outputSchema"];
|
|
1831
|
+
if (outputSchema === void 0) continue;
|
|
1832
|
+
const fieldPath = `accepts[${i}].outputSchema`;
|
|
1833
|
+
if (!isObject(outputSchema)) {
|
|
1834
|
+
issues.push({
|
|
1835
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA,
|
|
1836
|
+
field: fieldPath,
|
|
1837
|
+
message: ErrorMessages.INVALID_OUTPUT_SCHEMA,
|
|
1838
|
+
severity: "warning",
|
|
1839
|
+
fix: "Set outputSchema to an object with input and output properties"
|
|
1840
|
+
});
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const input = outputSchema["input"];
|
|
1844
|
+
if (!isObject(input) || !input["type"] || !input["method"]) issues.push({
|
|
1845
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA_INPUT,
|
|
1846
|
+
field: `${fieldPath}.input`,
|
|
1847
|
+
message: ErrorMessages.INVALID_OUTPUT_SCHEMA_INPUT,
|
|
1848
|
+
severity: "warning",
|
|
1849
|
+
fix: "Add input.type (e.g. \"application/json\") and input.method (e.g. \"POST\")"
|
|
1850
|
+
});
|
|
1851
|
+
const output = outputSchema["output"];
|
|
1852
|
+
if (!isObject(output)) issues.push({
|
|
1853
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA,
|
|
1854
|
+
field: `${fieldPath}.output`,
|
|
1855
|
+
message: "accepts[i].outputSchema.output must be an object",
|
|
1856
|
+
severity: "warning",
|
|
1857
|
+
fix: "Add an output object describing the API response format"
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
return issues;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Emit a warning when neither `extensions.bazaar` nor any `accepts[].outputSchema` is present.
|
|
1864
|
+
*
|
|
1865
|
+
* @returns Single-element array with MISSING_INPUT_SCHEMA warning, or empty array
|
|
1866
|
+
*/
|
|
1867
|
+
function validateMissingSchema(config, parsed) {
|
|
1868
|
+
if (config.extensions && config.extensions["bazaar"] !== void 0) return [];
|
|
1869
|
+
const accepts = parsed["accepts"];
|
|
1870
|
+
if (Array.isArray(accepts)) {
|
|
1871
|
+
for (const entry of accepts) if (isObject(entry) && entry["outputSchema"] !== void 0) return [];
|
|
1872
|
+
}
|
|
1873
|
+
return [{
|
|
1874
|
+
code: ErrorCode.MISSING_INPUT_SCHEMA,
|
|
1875
|
+
field: "extensions",
|
|
1876
|
+
message: ErrorMessages.MISSING_INPUT_SCHEMA,
|
|
1877
|
+
severity: "warning",
|
|
1878
|
+
fix: "Add extensions.bazaar with info and schema to help agents discover your API -- see https://bazaar.x402.org"
|
|
1879
|
+
}];
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
//#endregion
|
|
1883
|
+
//#region src/validation/orchestrator.ts
|
|
1884
|
+
/**
|
|
1885
|
+
* Validate an x402 config through the full pipeline.
|
|
1886
|
+
*
|
|
1887
|
+
* Takes any input (JSON string or object), runs it through:
|
|
1888
|
+
* 1. Structure validation (parse, object check, format detection)
|
|
1889
|
+
* 2. Normalization to canonical v2 shape
|
|
1890
|
+
* 3. Version, accepts, resource validation
|
|
1891
|
+
* 4. Per-entry field, network, asset, amount, timeout, address validation
|
|
1892
|
+
* 5. Legacy format warnings
|
|
1893
|
+
* 6. Strict mode promotion (warnings -> errors)
|
|
1894
|
+
*
|
|
1895
|
+
* NEVER throws -- all invalid inputs produce structured error results.
|
|
1896
|
+
*
|
|
1897
|
+
* @param input - JSON string or parsed object to validate
|
|
1898
|
+
* @param options - Validation options (e.g. strict mode)
|
|
1899
|
+
* @returns Structured validation result
|
|
1900
|
+
*/
|
|
1901
|
+
function validate(input, options) {
|
|
1902
|
+
try {
|
|
1903
|
+
return runPipeline(input, options);
|
|
1904
|
+
} catch {
|
|
1905
|
+
return {
|
|
1906
|
+
valid: false,
|
|
1907
|
+
version: "unknown",
|
|
1908
|
+
errors: [{
|
|
1909
|
+
code: ErrorCode.UNKNOWN_FORMAT,
|
|
1910
|
+
field: "$",
|
|
1911
|
+
message: "Unexpected validation error",
|
|
1912
|
+
severity: "error"
|
|
1913
|
+
}],
|
|
1914
|
+
warnings: [],
|
|
1915
|
+
normalized: null
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Internal pipeline implementation.
|
|
1921
|
+
* Separated from validate() so the try/catch safety net is clean.
|
|
1922
|
+
*/
|
|
1923
|
+
function runPipeline(input, options) {
|
|
1924
|
+
const structure = validateStructure(input);
|
|
1925
|
+
if (structure.issues.length > 0) return {
|
|
1926
|
+
valid: false,
|
|
1927
|
+
version: structure.format || "unknown",
|
|
1928
|
+
errors: structure.issues,
|
|
1929
|
+
warnings: [],
|
|
1930
|
+
normalized: null
|
|
1931
|
+
};
|
|
1932
|
+
const parsed = structure.parsed;
|
|
1933
|
+
const format = structure.format;
|
|
1934
|
+
const normalized = normalize$1(parsed);
|
|
1935
|
+
if (normalized === null) return {
|
|
1936
|
+
valid: false,
|
|
1937
|
+
version: format,
|
|
1938
|
+
errors: [{
|
|
1939
|
+
code: ErrorCode.UNKNOWN_FORMAT,
|
|
1940
|
+
field: "$",
|
|
1941
|
+
message: ErrorMessages.UNKNOWN_FORMAT,
|
|
1942
|
+
severity: "error"
|
|
1943
|
+
}],
|
|
1944
|
+
warnings: [],
|
|
1945
|
+
normalized: null
|
|
1946
|
+
};
|
|
1947
|
+
const errors = [];
|
|
1948
|
+
const warnings = [];
|
|
1949
|
+
errors.push(...validateVersion(normalized, format));
|
|
1950
|
+
errors.push(...validateAccepts(normalized));
|
|
1951
|
+
warnings.push(...validateResource(normalized, format));
|
|
1952
|
+
if (Array.isArray(normalized.accepts) && normalized.accepts.length > 0) for (let i = 0; i < normalized.accepts.length; i++) {
|
|
1953
|
+
const entry = normalized.accepts[i];
|
|
1954
|
+
const fieldPath = `accepts[${i}]`;
|
|
1955
|
+
errors.push(...validateFields(entry, fieldPath));
|
|
1956
|
+
for (const issue of validateNetwork(entry, fieldPath)) if (issue.severity === "error") errors.push(issue);
|
|
1957
|
+
else warnings.push(issue);
|
|
1958
|
+
warnings.push(...validateAsset(entry, fieldPath));
|
|
1959
|
+
errors.push(...validateAmount(entry, fieldPath));
|
|
1960
|
+
for (const issue of validateTimeout(entry, fieldPath, format)) if (issue.severity === "error") errors.push(issue);
|
|
1961
|
+
else warnings.push(issue);
|
|
1962
|
+
if (entry.payTo && entry.network) for (const issue of validateAddress(entry.payTo, entry.network, `${fieldPath}.payTo`)) if (issue.severity === "error") errors.push(issue);
|
|
1963
|
+
else warnings.push(issue);
|
|
1964
|
+
}
|
|
1965
|
+
warnings.push(...validateLegacy(normalized, format, parsed));
|
|
1966
|
+
warnings.push(...validateBazaar(normalized));
|
|
1967
|
+
warnings.push(...validateOutputSchema(parsed));
|
|
1968
|
+
warnings.push(...validateMissingSchema(normalized, parsed));
|
|
1969
|
+
if (options?.strict === true) {
|
|
1970
|
+
for (const warning of warnings) errors.push({
|
|
1971
|
+
...warning,
|
|
1972
|
+
severity: "error"
|
|
1973
|
+
});
|
|
1974
|
+
warnings.length = 0;
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
valid: errors.length === 0,
|
|
1978
|
+
version: format,
|
|
1979
|
+
errors,
|
|
1980
|
+
warnings,
|
|
1981
|
+
normalized
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
//#endregion
|
|
1986
|
+
//#region src/extraction/extract.ts
|
|
1987
|
+
/**
|
|
1988
|
+
* Get a header value, case-insensitive.
|
|
1989
|
+
* Supports both Headers objects and plain Record<string, string>.
|
|
1990
|
+
*/
|
|
1991
|
+
function getHeader(headers, name) {
|
|
1992
|
+
if (!headers) return null;
|
|
1993
|
+
if (typeof headers.get === "function") return headers.get(name);
|
|
1994
|
+
const lower = name.toLowerCase();
|
|
1995
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === lower) return headers[key];
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Decode base64 string to UTF-8 text.
|
|
2000
|
+
* Works in both browser (atob) and Node (Buffer).
|
|
2001
|
+
*/
|
|
2002
|
+
function decodeBase64(encoded) {
|
|
2003
|
+
if (typeof atob === "function") return atob(encoded);
|
|
2004
|
+
return Buffer.from(encoded, "base64").toString("utf-8");
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Check if a parsed object looks like it contains x402 config fields.
|
|
2008
|
+
*/
|
|
2009
|
+
function hasX402Fields(obj) {
|
|
2010
|
+
if (!obj || typeof obj !== "object") return false;
|
|
2011
|
+
const rec = obj;
|
|
2012
|
+
return !!(rec.accepts || rec.payTo || rec.x402Version);
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Try to parse the PAYMENT-REQUIRED header value as a base64-encoded JSON config.
|
|
2016
|
+
*/
|
|
2017
|
+
function tryHeaderExtraction(headers) {
|
|
2018
|
+
const headerValue = getHeader(headers, "payment-required");
|
|
2019
|
+
if (!headerValue) return null;
|
|
2020
|
+
try {
|
|
2021
|
+
const decoded = decodeBase64(headerValue);
|
|
2022
|
+
const parsed = JSON.parse(decoded);
|
|
2023
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return {
|
|
2024
|
+
config: parsed,
|
|
2025
|
+
source: "header"
|
|
2026
|
+
};
|
|
2027
|
+
} catch {}
|
|
2028
|
+
try {
|
|
2029
|
+
const parsed = JSON.parse(headerValue);
|
|
2030
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return {
|
|
2031
|
+
config: parsed,
|
|
2032
|
+
source: "header"
|
|
2033
|
+
};
|
|
2034
|
+
} catch {}
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Extract an x402 config from an HTTP 402 response.
|
|
2039
|
+
*
|
|
2040
|
+
* Extraction priority:
|
|
2041
|
+
* 1. JSON body — if it parses and has x402 fields (accepts, payTo, x402Version)
|
|
2042
|
+
* 2. PAYMENT-REQUIRED header — base64-decoded JSON fallback
|
|
2043
|
+
*
|
|
2044
|
+
* Never throws. Returns structured result with error message on failure.
|
|
2045
|
+
*
|
|
2046
|
+
* @param response - Response-like object with body and/or headers
|
|
2047
|
+
* @returns Extraction result with config, source, and error
|
|
2048
|
+
*/
|
|
2049
|
+
function extractConfig(response) {
|
|
2050
|
+
const body = response.body;
|
|
2051
|
+
if (body && typeof body === "object" && !Array.isArray(body) && hasX402Fields(body)) return {
|
|
2052
|
+
config: body,
|
|
2053
|
+
source: "body",
|
|
2054
|
+
error: null
|
|
2055
|
+
};
|
|
2056
|
+
if (typeof body === "string" && body.trim()) try {
|
|
2057
|
+
const parsed = JSON.parse(body);
|
|
2058
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && hasX402Fields(parsed)) return {
|
|
2059
|
+
config: parsed,
|
|
2060
|
+
source: "body",
|
|
2061
|
+
error: null
|
|
2062
|
+
};
|
|
2063
|
+
} catch {}
|
|
2064
|
+
const headerResult = tryHeaderExtraction(response.headers);
|
|
2065
|
+
if (headerResult) return {
|
|
2066
|
+
config: headerResult.config,
|
|
2067
|
+
source: headerResult.source,
|
|
2068
|
+
error: null
|
|
2069
|
+
};
|
|
2070
|
+
return {
|
|
2071
|
+
config: null,
|
|
2072
|
+
source: null,
|
|
2073
|
+
error: "No x402 config found in response body or PAYMENT-REQUIRED header"
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
//#endregion
|
|
2078
|
+
//#region src/check.ts
|
|
2079
|
+
/**
|
|
2080
|
+
* Check an HTTP 402 response: extract config, validate, and enrich with registry data.
|
|
2081
|
+
*
|
|
2082
|
+
* Never throws. All failures are represented in the returned CheckResult.
|
|
2083
|
+
*
|
|
2084
|
+
* @param response - Response-like object with body and/or headers
|
|
2085
|
+
* @param options - Validation options (e.g. strict mode)
|
|
2086
|
+
* @returns Unified check result
|
|
2087
|
+
*/
|
|
2088
|
+
function check(response, options) {
|
|
2089
|
+
const extraction = extractConfig(response);
|
|
2090
|
+
if (!extraction.config) return {
|
|
2091
|
+
extracted: false,
|
|
2092
|
+
source: null,
|
|
2093
|
+
extractionError: extraction.error,
|
|
2094
|
+
valid: false,
|
|
2095
|
+
version: "unknown",
|
|
2096
|
+
errors: [],
|
|
2097
|
+
warnings: [],
|
|
2098
|
+
normalized: null,
|
|
2099
|
+
summary: [],
|
|
2100
|
+
raw: null
|
|
2101
|
+
};
|
|
2102
|
+
const validation = validate(extraction.config, options);
|
|
2103
|
+
const summary = [];
|
|
2104
|
+
const accepts = validation.normalized?.accepts ?? [];
|
|
2105
|
+
for (let i = 0; i < accepts.length; i++) {
|
|
2106
|
+
const entry = accepts[i];
|
|
2107
|
+
const networkInfo = getNetworkInfo(entry.network);
|
|
2108
|
+
const assetInfo = getAssetInfo(entry.network, entry.asset);
|
|
2109
|
+
summary.push({
|
|
2110
|
+
index: i,
|
|
2111
|
+
network: entry.network,
|
|
2112
|
+
networkName: networkInfo?.name ?? entry.network,
|
|
2113
|
+
networkType: networkInfo?.type ?? null,
|
|
2114
|
+
payTo: entry.payTo,
|
|
2115
|
+
amount: entry.amount,
|
|
2116
|
+
asset: entry.asset,
|
|
2117
|
+
assetSymbol: assetInfo?.symbol ?? null,
|
|
2118
|
+
assetDecimals: assetInfo?.decimals ?? null,
|
|
2119
|
+
scheme: entry.scheme
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
return {
|
|
2123
|
+
extracted: true,
|
|
2124
|
+
source: extraction.source,
|
|
2125
|
+
extractionError: null,
|
|
2126
|
+
valid: validation.valid,
|
|
2127
|
+
version: validation.version,
|
|
2128
|
+
errors: validation.errors,
|
|
2129
|
+
warnings: validation.warnings,
|
|
2130
|
+
normalized: validation.normalized,
|
|
2131
|
+
summary,
|
|
2132
|
+
raw: extraction.config
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region src/index.ts
|
|
2138
|
+
const VERSION = "0.3.1";
|
|
2139
|
+
|
|
2140
|
+
//#endregion
|
|
2141
|
+
//#region src/cli.ts
|
|
2142
|
+
/**
|
|
2143
|
+
* x402check CLI
|
|
2144
|
+
*
|
|
2145
|
+
* Validate x402 payment configurations from the command line.
|
|
2146
|
+
*
|
|
2147
|
+
* Usage:
|
|
2148
|
+
* x402check <json> Validate inline JSON
|
|
2149
|
+
* x402check <file.json> Validate a JSON file
|
|
2150
|
+
* x402check <url> Fetch URL, extract + validate 402 config
|
|
2151
|
+
* echo '{}' | x402check Validate from stdin
|
|
2152
|
+
* x402check --version Print version
|
|
2153
|
+
* x402check --help Print help
|
|
2154
|
+
*
|
|
2155
|
+
* Flags:
|
|
2156
|
+
* --strict Promote all warnings to errors
|
|
2157
|
+
* --json Output raw JSON (for piping)
|
|
2158
|
+
* --quiet Only print errors (exit code only)
|
|
2159
|
+
*/
|
|
2160
|
+
function parseArgs(argv) {
|
|
2161
|
+
const args = {
|
|
2162
|
+
input: null,
|
|
2163
|
+
strict: false,
|
|
2164
|
+
json: false,
|
|
2165
|
+
quiet: false,
|
|
2166
|
+
help: false,
|
|
2167
|
+
version: false
|
|
2168
|
+
};
|
|
2169
|
+
for (const arg of argv) if (arg === "--strict") args.strict = true;
|
|
2170
|
+
else if (arg === "--json") args.json = true;
|
|
2171
|
+
else if (arg === "--quiet" || arg === "-q") args.quiet = true;
|
|
2172
|
+
else if (arg === "--help" || arg === "-h") args.help = true;
|
|
2173
|
+
else if (arg === "--version" || arg === "-v") args.version = true;
|
|
2174
|
+
else if (!arg.startsWith("-")) args.input = arg;
|
|
2175
|
+
return args;
|
|
2176
|
+
}
|
|
2177
|
+
const HELP = `x402check v${VERSION} — validate x402 payment configurations
|
|
2178
|
+
|
|
2179
|
+
Usage:
|
|
2180
|
+
x402check <json> Validate inline JSON string
|
|
2181
|
+
x402check <file.json> Validate a JSON file
|
|
2182
|
+
x402check <url> Fetch URL and check 402 response
|
|
2183
|
+
echo '...' | x402check Validate from stdin
|
|
2184
|
+
|
|
2185
|
+
Flags:
|
|
2186
|
+
--strict Promote all warnings to errors
|
|
2187
|
+
--json Output raw JSON result
|
|
2188
|
+
--quiet Suppress output, exit code only
|
|
2189
|
+
-h, --help Show this help
|
|
2190
|
+
-v, --version Show version
|
|
2191
|
+
|
|
2192
|
+
Exit codes:
|
|
2193
|
+
0 Valid config (or --help/--version)
|
|
2194
|
+
1 Invalid config or errors found
|
|
2195
|
+
2 Input error (no input, bad file, fetch failure)
|
|
2196
|
+
|
|
2197
|
+
Examples:
|
|
2198
|
+
x402check '{"x402Version":2,"accepts":[...]}'
|
|
2199
|
+
x402check config.json
|
|
2200
|
+
x402check https://api.example.com/resource --strict
|
|
2201
|
+
curl -s https://example.com | x402check --json
|
|
2202
|
+
`;
|
|
2203
|
+
function isUrl(s) {
|
|
2204
|
+
return /^https?:\/\//i.test(s);
|
|
2205
|
+
}
|
|
2206
|
+
function isJsonLike(s) {
|
|
2207
|
+
const trimmed = s.trim();
|
|
2208
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
2209
|
+
}
|
|
2210
|
+
function readStdin() {
|
|
2211
|
+
return new Promise((resolve, reject) => {
|
|
2212
|
+
const chunks = [];
|
|
2213
|
+
const { stdin } = process;
|
|
2214
|
+
if (stdin.isTTY) {
|
|
2215
|
+
resolve("");
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
stdin.on("data", (chunk) => chunks.push(chunk));
|
|
2219
|
+
stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
2220
|
+
stdin.on("error", reject);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
async function fetchUrl(url) {
|
|
2224
|
+
const res = await fetch(url);
|
|
2225
|
+
const headers = {};
|
|
2226
|
+
res.headers.forEach((value, key) => {
|
|
2227
|
+
headers[key] = value;
|
|
2228
|
+
});
|
|
2229
|
+
let body;
|
|
2230
|
+
if ((res.headers.get("content-type") || "").includes("json")) body = await res.json();
|
|
2231
|
+
else body = await res.text();
|
|
2232
|
+
return {
|
|
2233
|
+
status: res.status,
|
|
2234
|
+
body,
|
|
2235
|
+
headers
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
function formatIssue(issue) {
|
|
2239
|
+
const line = ` ${issue.severity === "error" ? "\x1B[31m✗\x1B[0m" : "\x1B[33m⚠\x1B[0m"} ${issue.code} [${issue.field}]: ${issue.message}`;
|
|
2240
|
+
if (issue.fix) return line + `\n ↳ ${issue.fix}`;
|
|
2241
|
+
return line;
|
|
2242
|
+
}
|
|
2243
|
+
function formatValidationResult(result, args) {
|
|
2244
|
+
if (args.json) return JSON.stringify(result, null, 2);
|
|
2245
|
+
if (args.quiet) return "";
|
|
2246
|
+
const lines = [];
|
|
2247
|
+
if (result.valid) lines.push(`\x1b[32m✓ Valid\x1b[0m x402 config (${result.version})`);
|
|
2248
|
+
else lines.push(`\x1b[31m✗ Invalid\x1b[0m x402 config (${result.version})`);
|
|
2249
|
+
if (result.errors.length > 0) {
|
|
2250
|
+
lines.push("");
|
|
2251
|
+
lines.push(`Errors (${result.errors.length}):`);
|
|
2252
|
+
for (const e of result.errors) lines.push(formatIssue(e));
|
|
2253
|
+
}
|
|
2254
|
+
if (result.warnings.length > 0) {
|
|
2255
|
+
lines.push("");
|
|
2256
|
+
lines.push(`Warnings (${result.warnings.length}):`);
|
|
2257
|
+
for (const w of result.warnings) lines.push(formatIssue(w));
|
|
2258
|
+
}
|
|
2259
|
+
return lines.join("\n");
|
|
2260
|
+
}
|
|
2261
|
+
function formatCheckResult(result, args) {
|
|
2262
|
+
if (args.json) return JSON.stringify(result, null, 2);
|
|
2263
|
+
if (args.quiet) return "";
|
|
2264
|
+
const lines = [];
|
|
2265
|
+
if (!result.extracted) {
|
|
2266
|
+
lines.push(`\x1b[31m✗ No x402 config found\x1b[0m`);
|
|
2267
|
+
if (result.extractionError) lines.push(` ${result.extractionError}`);
|
|
2268
|
+
return lines.join("\n");
|
|
2269
|
+
}
|
|
2270
|
+
lines.push(`Extracted from: ${result.source}`);
|
|
2271
|
+
if (result.valid) lines.push(`\x1b[32m✓ Valid\x1b[0m x402 config (${result.version})`);
|
|
2272
|
+
else lines.push(`\x1b[31m✗ Invalid\x1b[0m x402 config (${result.version})`);
|
|
2273
|
+
if (result.summary.length > 0) {
|
|
2274
|
+
lines.push("");
|
|
2275
|
+
lines.push("Payment options:");
|
|
2276
|
+
for (const s of result.summary) {
|
|
2277
|
+
const symbol = s.assetSymbol ?? s.asset;
|
|
2278
|
+
const net = s.networkName;
|
|
2279
|
+
lines.push(` [${s.index}] ${s.amount} ${symbol} on ${net} → ${s.payTo.slice(0, 10)}...`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (result.errors.length > 0) {
|
|
2283
|
+
lines.push("");
|
|
2284
|
+
lines.push(`Errors (${result.errors.length}):`);
|
|
2285
|
+
for (const e of result.errors) lines.push(formatIssue(e));
|
|
2286
|
+
}
|
|
2287
|
+
if (result.warnings.length > 0) {
|
|
2288
|
+
lines.push("");
|
|
2289
|
+
lines.push(`Warnings (${result.warnings.length}):`);
|
|
2290
|
+
for (const w of result.warnings) lines.push(formatIssue(w));
|
|
2291
|
+
}
|
|
2292
|
+
return lines.join("\n");
|
|
2293
|
+
}
|
|
2294
|
+
async function main() {
|
|
2295
|
+
const args = parseArgs(process.argv.slice(2));
|
|
2296
|
+
if (args.version) {
|
|
2297
|
+
console.log(VERSION);
|
|
2298
|
+
return 0;
|
|
2299
|
+
}
|
|
2300
|
+
if (args.help) {
|
|
2301
|
+
console.log(HELP);
|
|
2302
|
+
return 0;
|
|
2303
|
+
}
|
|
2304
|
+
let input = args.input;
|
|
2305
|
+
if (!input) {
|
|
2306
|
+
const stdinData = await readStdin();
|
|
2307
|
+
if (stdinData.trim()) input = stdinData.trim();
|
|
2308
|
+
}
|
|
2309
|
+
if (!input) {
|
|
2310
|
+
console.error("No input provided. Run x402check --help for usage.");
|
|
2311
|
+
return 2;
|
|
2312
|
+
}
|
|
2313
|
+
if (isUrl(input)) try {
|
|
2314
|
+
const { status, body, headers } = await fetchUrl(input);
|
|
2315
|
+
if (status !== 402) {
|
|
2316
|
+
if (!args.quiet) console.log(`HTTP ${status} (expected 402)`);
|
|
2317
|
+
}
|
|
2318
|
+
const result = check({
|
|
2319
|
+
body,
|
|
2320
|
+
headers
|
|
2321
|
+
}, { strict: args.strict });
|
|
2322
|
+
const output = formatCheckResult(result, args);
|
|
2323
|
+
if (output) console.log(output);
|
|
2324
|
+
return result.valid ? 0 : 1;
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
console.error(`Fetch failed: ${err.message}`);
|
|
2327
|
+
return 2;
|
|
2328
|
+
}
|
|
2329
|
+
if (!isJsonLike(input)) {
|
|
2330
|
+
const filePath = resolve$1(input);
|
|
2331
|
+
if (!existsSync(filePath)) {
|
|
2332
|
+
console.error(`File not found: ${filePath}`);
|
|
2333
|
+
return 2;
|
|
2334
|
+
}
|
|
2335
|
+
try {
|
|
2336
|
+
input = readFileSync(filePath, "utf-8");
|
|
2337
|
+
} catch (err) {
|
|
2338
|
+
console.error(`Cannot read file: ${err.message}`);
|
|
2339
|
+
return 2;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const result = validate(input, { strict: args.strict });
|
|
2343
|
+
const output = formatValidationResult(result, args);
|
|
2344
|
+
if (output) console.log(output);
|
|
2345
|
+
return result.valid ? 0 : 1;
|
|
2346
|
+
}
|
|
2347
|
+
main().then((code) => process.exit(code), (err) => {
|
|
2348
|
+
console.error(`Unexpected error: ${err.message}`);
|
|
2349
|
+
process.exit(2);
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
//#endregion
|
|
2353
|
+
export { };
|