x402check 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/dist/index.cjs +216 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +58 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.iife.js +2 -2
- package/dist/index.js +216 -2
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# x402check
|
|
2
|
+
|
|
3
|
+
Validate [x402](https://www.x402.org/) payment configurations. Works in Node, browsers, and edge runtimes — zero dependencies.
|
|
4
|
+
|
|
5
|
+
**[x402check.com](https://www.x402check.com)** — try it in the browser
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i x402check
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { validate } from 'x402check'
|
|
17
|
+
|
|
18
|
+
const result = validate({
|
|
19
|
+
x402Version: 2,
|
|
20
|
+
accepts: [{
|
|
21
|
+
scheme: 'exact',
|
|
22
|
+
network: 'base',
|
|
23
|
+
payTo: '0x1234567890abcdef1234567890abcdef12345678',
|
|
24
|
+
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
25
|
+
amount: '10000',
|
|
26
|
+
maxTimeoutSeconds: 300
|
|
27
|
+
}]
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
result.valid // true | false
|
|
31
|
+
result.errors // ValidationIssue[]
|
|
32
|
+
result.warnings // ValidationIssue[]
|
|
33
|
+
result.normalized // canonical v2 config
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### `validate(input, options?)`
|
|
39
|
+
|
|
40
|
+
Validates a config object or JSON string. Returns errors, warnings, and a normalized v2 config.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
validate(configOrJson)
|
|
44
|
+
validate(configOrJson, { strict: true }) // promotes warnings to errors
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Returns:** `ValidationResult`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
{
|
|
51
|
+
valid: boolean
|
|
52
|
+
version: 'v2' | 'v1' | 'unknown'
|
|
53
|
+
errors: ValidationIssue[]
|
|
54
|
+
warnings: ValidationIssue[]
|
|
55
|
+
normalized: NormalizedConfig | null
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each issue includes a machine-readable `code`, a `field` path, a human-readable `message`, and an optional `fix` suggestion.
|
|
60
|
+
|
|
61
|
+
### `extractConfig(response)`
|
|
62
|
+
|
|
63
|
+
Extracts an x402 config from an HTTP 402 response. Checks the JSON body first, then falls back to the `PAYMENT-REQUIRED` header (base64 or raw JSON).
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const res = await fetch(url)
|
|
67
|
+
const { config, source, error } = extractConfig({
|
|
68
|
+
body: await res.json(),
|
|
69
|
+
headers: res.headers
|
|
70
|
+
})
|
|
71
|
+
// source: 'body' | 'header' | null
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `detect(input)`
|
|
75
|
+
|
|
76
|
+
Returns the config format: `'v2'`, `'v1'`, or `'unknown'`.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
detect({ x402Version: 2, accepts: [...] }) // 'v2'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `normalize(input)`
|
|
83
|
+
|
|
84
|
+
Converts any supported config to canonical v2 shape. Returns `null` if the format is unrecognized.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const v2Config = normalize(v1Config)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Address validation
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { validateAddress, validateEvmAddress, validateSolanaAddress } from 'x402check'
|
|
94
|
+
|
|
95
|
+
validateAddress(addr, 'eip155:8453', 'payTo') // dispatches by network
|
|
96
|
+
validateEvmAddress(addr, 'payTo') // EIP-55 checksum verification
|
|
97
|
+
validateSolanaAddress(addr, 'payTo') // base58, 32-byte decode check
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Network & asset registry
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import {
|
|
104
|
+
isKnownNetwork, getNetworkInfo, getCanonicalNetwork,
|
|
105
|
+
isKnownAsset, getAssetInfo, isValidCaip2
|
|
106
|
+
} from 'x402check'
|
|
107
|
+
|
|
108
|
+
isValidCaip2('eip155:8453') // true
|
|
109
|
+
getCanonicalNetwork('base') // 'eip155:8453'
|
|
110
|
+
getNetworkInfo('eip155:8453') // { name: 'Base', type: 'evm', testnet: false }
|
|
111
|
+
isKnownAsset('eip155:8453', '0x833…') // true
|
|
112
|
+
getAssetInfo('eip155:8453', '0x833…') // { symbol: 'USDC', name: 'USD Coin', decimals: 6 }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Supported formats
|
|
116
|
+
|
|
117
|
+
| Format | `x402Version` | Status |
|
|
118
|
+
|--------|---------------|--------|
|
|
119
|
+
| v2 | `2` | Recommended |
|
|
120
|
+
| v1 | `1` | Supported, auto-normalized to v2 |
|
|
121
|
+
|
|
122
|
+
## Validation checks
|
|
123
|
+
|
|
124
|
+
- Required fields (`scheme`, `network`, `amount`, `asset`, `payTo`)
|
|
125
|
+
- Amount is a numeric string > 0
|
|
126
|
+
- Network is valid [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md)
|
|
127
|
+
- EVM addresses: `0x`-prefixed, 40 hex chars, EIP-55 checksum
|
|
128
|
+
- Solana addresses: base58, decodes to 32 bytes
|
|
129
|
+
- Known network and asset registry warnings
|
|
130
|
+
- `maxTimeoutSeconds` is a positive integer (if present)
|
|
131
|
+
- Resource URL format (if present)
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,12 @@ const ErrorCode = {
|
|
|
28
28
|
NO_EVM_CHECKSUM: "NO_EVM_CHECKSUM",
|
|
29
29
|
INVALID_SOLANA_ADDRESS: "INVALID_SOLANA_ADDRESS",
|
|
30
30
|
ADDRESS_NETWORK_MISMATCH: "ADDRESS_NETWORK_MISMATCH",
|
|
31
|
+
INVALID_BAZAAR_INFO: "INVALID_BAZAAR_INFO",
|
|
32
|
+
INVALID_BAZAAR_SCHEMA: "INVALID_BAZAAR_SCHEMA",
|
|
33
|
+
INVALID_BAZAAR_INFO_INPUT: "INVALID_BAZAAR_INFO_INPUT",
|
|
34
|
+
INVALID_OUTPUT_SCHEMA: "INVALID_OUTPUT_SCHEMA",
|
|
35
|
+
INVALID_OUTPUT_SCHEMA_INPUT: "INVALID_OUTPUT_SCHEMA_INPUT",
|
|
36
|
+
MISSING_INPUT_SCHEMA: "MISSING_INPUT_SCHEMA",
|
|
31
37
|
UNKNOWN_NETWORK: "UNKNOWN_NETWORK",
|
|
32
38
|
UNKNOWN_ASSET: "UNKNOWN_ASSET",
|
|
33
39
|
LEGACY_FORMAT: "LEGACY_FORMAT",
|
|
@@ -61,6 +67,12 @@ const ErrorMessages = {
|
|
|
61
67
|
NO_EVM_CHECKSUM: "EVM address is all-lowercase with no checksum protection",
|
|
62
68
|
INVALID_SOLANA_ADDRESS: "Invalid Solana address format",
|
|
63
69
|
ADDRESS_NETWORK_MISMATCH: "Address format does not match network type",
|
|
70
|
+
INVALID_BAZAAR_INFO: "extensions.bazaar.info must be an object with input and output",
|
|
71
|
+
INVALID_BAZAAR_SCHEMA: "extensions.bazaar.schema must be a valid JSON Schema object",
|
|
72
|
+
INVALID_BAZAAR_INFO_INPUT: "extensions.bazaar.info.input must include type and method",
|
|
73
|
+
INVALID_OUTPUT_SCHEMA: "accepts[i].outputSchema must be an object with input and output",
|
|
74
|
+
INVALID_OUTPUT_SCHEMA_INPUT: "accepts[i].outputSchema.input must include type and method",
|
|
75
|
+
MISSING_INPUT_SCHEMA: "No input schema found (no bazaar extension or outputSchema) -- consider adding one so agents know how to call your API",
|
|
64
76
|
UNKNOWN_NETWORK: "Network is not in the known registry -- config may still work but cannot be fully validated",
|
|
65
77
|
UNKNOWN_ASSET: "Asset is not in the known registry -- config may still work but cannot be fully validated",
|
|
66
78
|
LEGACY_FORMAT: "Config uses legacy flat format -- consider upgrading to x402 v2",
|
|
@@ -1724,6 +1736,146 @@ function validateLegacy(_config, detectedFormat, _originalInput) {
|
|
|
1724
1736
|
return issues;
|
|
1725
1737
|
}
|
|
1726
1738
|
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/validation/rules/extensions.ts
|
|
1741
|
+
/**
|
|
1742
|
+
* Check whether a value is a non-null plain object (not an array).
|
|
1743
|
+
*/
|
|
1744
|
+
function isObject(v) {
|
|
1745
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Validate `extensions.bazaar` when present.
|
|
1749
|
+
*
|
|
1750
|
+
* Checks:
|
|
1751
|
+
* - bazaar is an object
|
|
1752
|
+
* - bazaar.info exists and is an object with input (type + method) and output
|
|
1753
|
+
* - bazaar.schema exists and looks like a JSON Schema object
|
|
1754
|
+
*
|
|
1755
|
+
* @returns Array of warning issues (empty when bazaar is absent or valid)
|
|
1756
|
+
*/
|
|
1757
|
+
function validateBazaar(config) {
|
|
1758
|
+
const issues = [];
|
|
1759
|
+
if (!config.extensions) return issues;
|
|
1760
|
+
const bazaar = config.extensions["bazaar"];
|
|
1761
|
+
if (bazaar === void 0) return issues;
|
|
1762
|
+
if (!isObject(bazaar)) {
|
|
1763
|
+
issues.push({
|
|
1764
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1765
|
+
field: "extensions.bazaar",
|
|
1766
|
+
message: "extensions.bazaar must be an object",
|
|
1767
|
+
severity: "warning",
|
|
1768
|
+
fix: "Set extensions.bazaar to an object with info and schema properties"
|
|
1769
|
+
});
|
|
1770
|
+
return issues;
|
|
1771
|
+
}
|
|
1772
|
+
const info = bazaar["info"];
|
|
1773
|
+
if (!isObject(info)) issues.push({
|
|
1774
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1775
|
+
field: "extensions.bazaar.info",
|
|
1776
|
+
message: ErrorMessages.INVALID_BAZAAR_INFO,
|
|
1777
|
+
severity: "warning",
|
|
1778
|
+
fix: "Add an info object with input and output properties describing your API"
|
|
1779
|
+
});
|
|
1780
|
+
else {
|
|
1781
|
+
const input = info["input"];
|
|
1782
|
+
if (!isObject(input) || !input["type"] || !input["method"]) issues.push({
|
|
1783
|
+
code: ErrorCode.INVALID_BAZAAR_INFO_INPUT,
|
|
1784
|
+
field: "extensions.bazaar.info.input",
|
|
1785
|
+
message: ErrorMessages.INVALID_BAZAAR_INFO_INPUT,
|
|
1786
|
+
severity: "warning",
|
|
1787
|
+
fix: "Add input.type (e.g. \"application/json\") and input.method (e.g. \"POST\")"
|
|
1788
|
+
});
|
|
1789
|
+
const output = info["output"];
|
|
1790
|
+
if (!isObject(output)) issues.push({
|
|
1791
|
+
code: ErrorCode.INVALID_BAZAAR_INFO,
|
|
1792
|
+
field: "extensions.bazaar.info.output",
|
|
1793
|
+
message: "extensions.bazaar.info.output must be an object",
|
|
1794
|
+
severity: "warning",
|
|
1795
|
+
fix: "Add an output object describing the API response format"
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
const schema = bazaar["schema"];
|
|
1799
|
+
if (!isObject(schema) || !schema["type"] && !schema["$schema"] && !schema["properties"]) issues.push({
|
|
1800
|
+
code: ErrorCode.INVALID_BAZAAR_SCHEMA,
|
|
1801
|
+
field: "extensions.bazaar.schema",
|
|
1802
|
+
message: ErrorMessages.INVALID_BAZAAR_SCHEMA,
|
|
1803
|
+
severity: "warning",
|
|
1804
|
+
fix: "Add a JSON Schema object with type, $schema, or properties"
|
|
1805
|
+
});
|
|
1806
|
+
return issues;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Validate `accepts[].outputSchema` on the raw parsed input.
|
|
1810
|
+
*
|
|
1811
|
+
* Uses the raw parsed object because AcceptsEntry strips outputSchema during normalization.
|
|
1812
|
+
*
|
|
1813
|
+
* Checks per entry with outputSchema:
|
|
1814
|
+
* - outputSchema is an object
|
|
1815
|
+
* - outputSchema.input exists with type and method
|
|
1816
|
+
* - outputSchema.output exists and is an object
|
|
1817
|
+
*
|
|
1818
|
+
* @returns Array of warning issues
|
|
1819
|
+
*/
|
|
1820
|
+
function validateOutputSchema(parsed) {
|
|
1821
|
+
const issues = [];
|
|
1822
|
+
const accepts = parsed["accepts"];
|
|
1823
|
+
if (!Array.isArray(accepts)) return issues;
|
|
1824
|
+
for (let i = 0; i < accepts.length; i++) {
|
|
1825
|
+
const entry = accepts[i];
|
|
1826
|
+
if (!isObject(entry)) continue;
|
|
1827
|
+
const outputSchema = entry["outputSchema"];
|
|
1828
|
+
if (outputSchema === void 0) continue;
|
|
1829
|
+
const fieldPath = `accepts[${i}].outputSchema`;
|
|
1830
|
+
if (!isObject(outputSchema)) {
|
|
1831
|
+
issues.push({
|
|
1832
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA,
|
|
1833
|
+
field: fieldPath,
|
|
1834
|
+
message: ErrorMessages.INVALID_OUTPUT_SCHEMA,
|
|
1835
|
+
severity: "warning",
|
|
1836
|
+
fix: "Set outputSchema to an object with input and output properties"
|
|
1837
|
+
});
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const input = outputSchema["input"];
|
|
1841
|
+
if (!isObject(input) || !input["type"] || !input["method"]) issues.push({
|
|
1842
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA_INPUT,
|
|
1843
|
+
field: `${fieldPath}.input`,
|
|
1844
|
+
message: ErrorMessages.INVALID_OUTPUT_SCHEMA_INPUT,
|
|
1845
|
+
severity: "warning",
|
|
1846
|
+
fix: "Add input.type (e.g. \"application/json\") and input.method (e.g. \"POST\")"
|
|
1847
|
+
});
|
|
1848
|
+
const output = outputSchema["output"];
|
|
1849
|
+
if (!isObject(output)) issues.push({
|
|
1850
|
+
code: ErrorCode.INVALID_OUTPUT_SCHEMA,
|
|
1851
|
+
field: `${fieldPath}.output`,
|
|
1852
|
+
message: "accepts[i].outputSchema.output must be an object",
|
|
1853
|
+
severity: "warning",
|
|
1854
|
+
fix: "Add an output object describing the API response format"
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
return issues;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Emit a warning when neither `extensions.bazaar` nor any `accepts[].outputSchema` is present.
|
|
1861
|
+
*
|
|
1862
|
+
* @returns Single-element array with MISSING_INPUT_SCHEMA warning, or empty array
|
|
1863
|
+
*/
|
|
1864
|
+
function validateMissingSchema(config, parsed) {
|
|
1865
|
+
if (config.extensions && config.extensions["bazaar"] !== void 0) return [];
|
|
1866
|
+
const accepts = parsed["accepts"];
|
|
1867
|
+
if (Array.isArray(accepts)) {
|
|
1868
|
+
for (const entry of accepts) if (isObject(entry) && entry["outputSchema"] !== void 0) return [];
|
|
1869
|
+
}
|
|
1870
|
+
return [{
|
|
1871
|
+
code: ErrorCode.MISSING_INPUT_SCHEMA,
|
|
1872
|
+
field: "extensions",
|
|
1873
|
+
message: ErrorMessages.MISSING_INPUT_SCHEMA,
|
|
1874
|
+
severity: "warning",
|
|
1875
|
+
fix: "Add extensions.bazaar with info and schema to help agents discover your API -- see https://bazaar.x402.org"
|
|
1876
|
+
}];
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1727
1879
|
//#endregion
|
|
1728
1880
|
//#region src/validation/orchestrator.ts
|
|
1729
1881
|
/**
|
|
@@ -1808,6 +1960,9 @@ function runPipeline(input, options) {
|
|
|
1808
1960
|
else warnings.push(issue);
|
|
1809
1961
|
}
|
|
1810
1962
|
warnings.push(...validateLegacy(normalized, format, parsed));
|
|
1963
|
+
warnings.push(...validateBazaar(normalized));
|
|
1964
|
+
warnings.push(...validateOutputSchema(parsed));
|
|
1965
|
+
warnings.push(...validateMissingSchema(normalized, parsed));
|
|
1811
1966
|
if (options?.strict === true) {
|
|
1812
1967
|
for (const warning of warnings) errors.push({
|
|
1813
1968
|
...warning,
|
|
@@ -1916,9 +2071,68 @@ function extractConfig(response) {
|
|
|
1916
2071
|
};
|
|
1917
2072
|
}
|
|
1918
2073
|
|
|
2074
|
+
//#endregion
|
|
2075
|
+
//#region src/check.ts
|
|
2076
|
+
/**
|
|
2077
|
+
* Check an HTTP 402 response: extract config, validate, and enrich with registry data.
|
|
2078
|
+
*
|
|
2079
|
+
* Never throws. All failures are represented in the returned CheckResult.
|
|
2080
|
+
*
|
|
2081
|
+
* @param response - Response-like object with body and/or headers
|
|
2082
|
+
* @param options - Validation options (e.g. strict mode)
|
|
2083
|
+
* @returns Unified check result
|
|
2084
|
+
*/
|
|
2085
|
+
function check(response, options) {
|
|
2086
|
+
const extraction = extractConfig(response);
|
|
2087
|
+
if (!extraction.config) return {
|
|
2088
|
+
extracted: false,
|
|
2089
|
+
source: null,
|
|
2090
|
+
extractionError: extraction.error,
|
|
2091
|
+
valid: false,
|
|
2092
|
+
version: "unknown",
|
|
2093
|
+
errors: [],
|
|
2094
|
+
warnings: [],
|
|
2095
|
+
normalized: null,
|
|
2096
|
+
summary: [],
|
|
2097
|
+
raw: null
|
|
2098
|
+
};
|
|
2099
|
+
const validation = validate(extraction.config, options);
|
|
2100
|
+
const summary = [];
|
|
2101
|
+
const accepts = validation.normalized?.accepts ?? [];
|
|
2102
|
+
for (let i = 0; i < accepts.length; i++) {
|
|
2103
|
+
const entry = accepts[i];
|
|
2104
|
+
const networkInfo = getNetworkInfo(entry.network);
|
|
2105
|
+
const assetInfo = getAssetInfo(entry.network, entry.asset);
|
|
2106
|
+
summary.push({
|
|
2107
|
+
index: i,
|
|
2108
|
+
network: entry.network,
|
|
2109
|
+
networkName: networkInfo?.name ?? entry.network,
|
|
2110
|
+
networkType: networkInfo?.type ?? null,
|
|
2111
|
+
payTo: entry.payTo,
|
|
2112
|
+
amount: entry.amount,
|
|
2113
|
+
asset: entry.asset,
|
|
2114
|
+
assetSymbol: assetInfo?.symbol ?? null,
|
|
2115
|
+
assetDecimals: assetInfo?.decimals ?? null,
|
|
2116
|
+
scheme: entry.scheme
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
extracted: true,
|
|
2121
|
+
source: extraction.source,
|
|
2122
|
+
extractionError: null,
|
|
2123
|
+
valid: validation.valid,
|
|
2124
|
+
version: validation.version,
|
|
2125
|
+
errors: validation.errors,
|
|
2126
|
+
warnings: validation.warnings,
|
|
2127
|
+
normalized: validation.normalized,
|
|
2128
|
+
summary,
|
|
2129
|
+
raw: extraction.config
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1919
2133
|
//#endregion
|
|
1920
2134
|
//#region src/index.ts
|
|
1921
|
-
const VERSION = "0.
|
|
2135
|
+
const VERSION = "0.2.0";
|
|
1922
2136
|
|
|
1923
2137
|
//#endregion
|
|
1924
2138
|
exports.CAIP2_REGEX = CAIP2_REGEX;
|
|
@@ -1928,6 +2142,7 @@ exports.KNOWN_ASSETS = KNOWN_ASSETS;
|
|
|
1928
2142
|
exports.KNOWN_NETWORKS = KNOWN_NETWORKS;
|
|
1929
2143
|
exports.SIMPLE_NAME_TO_CAIP2 = SIMPLE_NAME_TO_CAIP2;
|
|
1930
2144
|
exports.VERSION = VERSION;
|
|
2145
|
+
exports.check = check;
|
|
1931
2146
|
exports.decodeBase58 = decodeBase58;
|
|
1932
2147
|
exports.detect = detect;
|
|
1933
2148
|
exports.extractConfig = extractConfig;
|