x402-surface-check 0.2.35 → 0.2.37

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 CHANGED
@@ -30,6 +30,8 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
30
30
  - No-payment HTTP 402 challenge shape
31
31
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
32
32
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
33
+ - x402 challenges nested inside framework error wrappers such as FastAPI-style `{"detail": {...}}`
34
+ - MPP descriptor-only 402s that advertise discovery headers but do not return a machine-readable payment retry challenge
33
35
  - Atomic-unit `amount` / `maxAmountRequired` fields, plus legacy decimal `amount` + `token` x402 v1 challenges
34
36
  - `asset` or token metadata, `network`, and `payTo`
35
37
  - OpenAPI-declared `x-payment-info.price.amount` drift versus the live 402 challenge price
@@ -648,6 +648,29 @@ function parseX402Authenticate(value) {
648
648
  }
649
649
  }
650
650
 
651
+ function bodyChallengeJson(value) {
652
+ if (!value || typeof value !== 'object') return null
653
+ if (Array.isArray(value.accepts) || Array.isArray(value.schemes)) return value
654
+
655
+ const candidates = [
656
+ value.detail,
657
+ value.error,
658
+ value.payment,
659
+ value.paymentRequired,
660
+ value.payment_required,
661
+ value.challenge,
662
+ value.x402,
663
+ ]
664
+
665
+ for (const candidate of candidates) {
666
+ if (!candidate || typeof candidate !== 'object') continue
667
+ const nested = bodyChallengeJson(candidate)
668
+ if (nested) return nested
669
+ }
670
+
671
+ return null
672
+ }
673
+
651
674
  async function fetchDocument(url) {
652
675
  const response = await fetch(url, {
653
676
  headers: {
@@ -750,7 +773,11 @@ async function probeEndpoint(entry, origin) {
750
773
  const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
751
774
  ?? parseX402Authenticate(response.headers.get('www-authenticate'))
752
775
 
753
- const bodyHasChallenge = Array.isArray(body.json?.accepts) || Array.isArray(body.json?.schemes)
776
+ const bodyChallenge = bodyChallengeJson(body.json)
777
+ const bodyHasChallenge = Boolean(bodyChallenge)
778
+ if (bodyChallenge && body.json !== bodyChallenge) {
779
+ body.json = bodyChallenge
780
+ }
754
781
  if (!bodyHasChallenge) {
755
782
  if (headerChallenge && typeof headerChallenge === 'object') {
756
783
  body.json = headerChallenge
@@ -1084,6 +1111,17 @@ function hasMppRetryChallenge(result) {
1084
1111
  return challengeAccepts(result).some(accept => String(accept.scheme ?? '').toLowerCase() === 'mpp')
1085
1112
  }
1086
1113
 
1114
+ function mppDiscoveryHeaders(headers = {}) {
1115
+ return [
1116
+ 'x-mpp-descriptor',
1117
+ 'x-agent-card',
1118
+ ].filter(name => headers[name] !== undefined && headers[name] !== '')
1119
+ }
1120
+
1121
+ function advertisesMppDiscovery(result) {
1122
+ return mppDiscoveryHeaders(result.headers).length > 0
1123
+ }
1124
+
1087
1125
  function allowsAnyHeader(headerValue = '', names = []) {
1088
1126
  return names.some(name => headerListAllows(headerValue, name))
1089
1127
  }
@@ -1160,6 +1198,10 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1160
1198
  }
1161
1199
 
1162
1200
  if (!hasChallenge) {
1201
+ const descriptorHeaders = mppDiscoveryHeaders(result.headers)
1202
+ if (result.status === 402 && descriptorHeaders.length > 0) {
1203
+ findings.push(`P1 - ${result.name} returns 402 and advertises MPP discovery via ${descriptorHeaders.join(', ')}, but does not include a WWW-Authenticate: Payment challenge or machine-readable body challenge; autonomous callers cannot construct a paid retry from this response alone.`)
1204
+ }
1163
1205
  continue
1164
1206
  }
1165
1207
 
@@ -1238,7 +1280,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1238
1280
 
1239
1281
  for (const result of preflightResults) {
1240
1282
  const challengeResult = challengesByEntry.get(entryKey(result))
1241
- if (!challengeResult || (!hasPaymentChallenge(challengeResult) && !advertisesPaymentEnforcement(challengeResult.headers))) continue
1283
+ if (!challengeResult || (!hasPaymentChallenge(challengeResult) && !advertisesPaymentEnforcement(challengeResult.headers) && !advertisesMppDiscovery(challengeResult))) continue
1242
1284
  const allowedOrigin = result.headers['access-control-allow-origin'] ?? ''
1243
1285
  if (!allowedOrigin) {
1244
1286
  findings.push(`P1 - ${result.name} CORS preflight does not allow the requesting origin; observed allow-origin: none.`)
@@ -1250,7 +1292,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1250
1292
  : `allow headers: ${allowed || 'none'}`
1251
1293
  findings.push(`P1 - ${result.name} CORS preflight does not allow a known x402 retry header (X-PAYMENT or PAYMENT-SIGNATURE); observed ${observed}.`)
1252
1294
  }
1253
- if (hasMppRetryChallenge(challengeResult) && !headerListAllows(allowed, 'authorization')) {
1295
+ if ((hasMppRetryChallenge(challengeResult) || advertisesMppDiscovery(challengeResult)) && !headerListAllows(allowed, 'authorization')) {
1254
1296
  const observed = result.status >= 400
1255
1297
  ? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
1256
1298
  : `allow headers: ${allowed || 'none'}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {