x402-surface-check 0.2.8 → 0.2.10
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 +2 -1
- package/bin/x402-surface-check.mjs +42 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,10 +16,11 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
16
16
|
|
|
17
17
|
- Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
|
|
18
18
|
- No-payment HTTP 402 challenge shape
|
|
19
|
-
- x402 v1 and v2 price fields
|
|
19
|
+
- x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
|
|
20
20
|
- MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
|
|
21
21
|
- Atomic-unit `amount` / `maxAmountRequired` fields, plus legacy decimal `amount` + `token` x402 v1 challenges
|
|
22
22
|
- `asset` or token metadata, `network`, and `payTo`
|
|
23
|
+
- OpenAPI-declared `x-payment-info.price.amount` drift versus the live 402 challenge price
|
|
23
24
|
- Placeholder recipients such as zero addresses and Solana system-program values
|
|
24
25
|
- Testnet or staging rails such as Base Sepolia and Solana devnet
|
|
25
26
|
- HTTPS resource URLs and stable resource metadata
|
|
@@ -98,6 +98,11 @@ function moneyFromDecimal(amount) {
|
|
|
98
98
|
})}`
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function numberFromDecimal(amount) {
|
|
102
|
+
const numeric = Number(amount)
|
|
103
|
+
return Number.isFinite(numeric) ? numeric : null
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
function uniqueEntries(entries, limit) {
|
|
102
107
|
const seen = new Set()
|
|
103
108
|
return entries
|
|
@@ -127,6 +132,15 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
|
|
|
127
132
|
return new URL(value, base).toString()
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
function operationExpectedPrice(operation) {
|
|
136
|
+
const price = operation?.['x-payment-info']?.price
|
|
137
|
+
?? operation?.['x-payment']?.price
|
|
138
|
+
?? operation?.payment?.price
|
|
139
|
+
const amount = price?.amount ?? price?.amountUsd ?? price?.usd
|
|
140
|
+
const numeric = numberFromDecimal(amount)
|
|
141
|
+
return numeric === null ? null : numeric
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
function endpointEntries(document, sourceUrl, limit) {
|
|
131
145
|
const entries = []
|
|
132
146
|
const baseUrl = documentBaseUrl(document, sourceUrl)
|
|
@@ -189,6 +203,7 @@ function endpointEntries(document, sourceUrl, limit) {
|
|
|
189
203
|
name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
|
|
190
204
|
url,
|
|
191
205
|
method: method.toUpperCase(),
|
|
206
|
+
expectedPriceUsd: operationExpectedPrice(operation),
|
|
192
207
|
})
|
|
193
208
|
}
|
|
194
209
|
}
|
|
@@ -332,8 +347,9 @@ async function probeEndpoint(entry) {
|
|
|
332
347
|
const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
|
|
333
348
|
?? parseX402Authenticate(response.headers.get('www-authenticate'))
|
|
334
349
|
|
|
335
|
-
|
|
336
|
-
|
|
350
|
+
const bodyHasChallenge = Array.isArray(body.json?.accepts) || Array.isArray(body.json?.schemes)
|
|
351
|
+
if (!bodyHasChallenge) {
|
|
352
|
+
if (headerChallenge && typeof headerChallenge === 'object') {
|
|
337
353
|
body.json = headerChallenge
|
|
338
354
|
}
|
|
339
355
|
else if (authenticateChallenge) {
|
|
@@ -400,7 +416,9 @@ function capabilityList(value) {
|
|
|
400
416
|
}
|
|
401
417
|
|
|
402
418
|
function challengeAccepts(result) {
|
|
403
|
-
|
|
419
|
+
if (Array.isArray(result.body.json?.accepts)) return result.body.json.accepts
|
|
420
|
+
if (Array.isArray(result.body.json?.schemes)) return result.body.json.schemes
|
|
421
|
+
return []
|
|
404
422
|
}
|
|
405
423
|
|
|
406
424
|
function acceptAmountValue(accept) {
|
|
@@ -418,10 +436,11 @@ function acceptDecimals(accept) {
|
|
|
418
436
|
}
|
|
419
437
|
|
|
420
438
|
function usesDecimalAmount(accept, result) {
|
|
421
|
-
|
|
422
|
-
if (
|
|
423
|
-
const amount = String(
|
|
439
|
+
const rawAmount = acceptAmountValue(accept)
|
|
440
|
+
if (rawAmount === undefined || rawAmount === null || rawAmount === '') return false
|
|
441
|
+
const amount = String(rawAmount)
|
|
424
442
|
if (amount.includes('.')) return true
|
|
443
|
+
if (accept.maxAmountRequired !== undefined || accept.maxAmount !== undefined) return false
|
|
425
444
|
if (!accept.asset && (accept.token || result.headers?.['x-payment-token'])) return true
|
|
426
445
|
return result.headers?.['x-payment-amount'] === amount
|
|
427
446
|
}
|
|
@@ -433,6 +452,14 @@ function challengePrice(accept, result) {
|
|
|
433
452
|
: moneyFromAtomic(amount, acceptDecimals(accept))
|
|
434
453
|
}
|
|
435
454
|
|
|
455
|
+
function challengePriceUsd(accept, result) {
|
|
456
|
+
const amount = acceptAmountValue(accept)
|
|
457
|
+
if (usesDecimalAmount(accept, result)) return numberFromDecimal(amount)
|
|
458
|
+
const numeric = Number(amount)
|
|
459
|
+
if (!Number.isFinite(numeric)) return null
|
|
460
|
+
return numeric / (10 ** acceptDecimals(accept))
|
|
461
|
+
}
|
|
462
|
+
|
|
436
463
|
function hasPaymentChallenge(result) {
|
|
437
464
|
const challenge = result.body.json
|
|
438
465
|
return challengeAccepts(result).length > 0 || Boolean(challenge?.resource || challenge?.payment || result.headers?.['www-authenticate'])
|
|
@@ -440,7 +467,7 @@ function hasPaymentChallenge(result) {
|
|
|
440
467
|
|
|
441
468
|
function challengeSummary(result) {
|
|
442
469
|
const challenge = result.body.json
|
|
443
|
-
const firstAccept =
|
|
470
|
+
const firstAccept = challengeAccepts(result)[0] ?? {}
|
|
444
471
|
const hasChallenge = hasPaymentChallenge(result)
|
|
445
472
|
const amount = acceptAmountValue(firstAccept)
|
|
446
473
|
const resourceUrl = challenge?.resource?.url ?? firstAccept.resource ?? ''
|
|
@@ -453,6 +480,8 @@ function challengeSummary(result) {
|
|
|
453
480
|
network: firstAccept.network ?? '',
|
|
454
481
|
amount,
|
|
455
482
|
price: hasChallenge ? challengePrice(firstAccept, result) : '',
|
|
483
|
+
priceUsd: hasChallenge ? challengePriceUsd(firstAccept, result) : null,
|
|
484
|
+
expectedPriceUsd: typeof result.expectedPriceUsd === 'number' ? result.expectedPriceUsd : null,
|
|
456
485
|
payTo: firstAccept.payTo ?? '',
|
|
457
486
|
asset: acceptAssetValue(firstAccept),
|
|
458
487
|
timeout: firstAccept.maxTimeoutSeconds ?? '',
|
|
@@ -520,6 +549,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
520
549
|
if (!summary.amount || !summary.payTo || !summary.asset) {
|
|
521
550
|
findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
|
|
522
551
|
}
|
|
552
|
+
if (summary.expectedPriceUsd !== null && summary.priceUsd !== null) {
|
|
553
|
+
const delta = Math.abs(summary.expectedPriceUsd - summary.priceUsd)
|
|
554
|
+
if (delta > 0.000001) {
|
|
555
|
+
findings.push(`P1 - ${result.name} documented price ${moneyFromDecimal(summary.expectedPriceUsd)} does not match live 402 challenge price ${moneyFromDecimal(summary.priceUsd)}.`)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
523
558
|
for (const accept of challengeAccepts(result)) {
|
|
524
559
|
if (looksLikePlaceholderPayTo(accept.payTo)) {
|
|
525
560
|
findings.push(`P1 - ${result.name} challenge advertises placeholder-looking payTo ${accept.payTo}; production listings should not ask agents to pay placeholder recipients.`)
|