x402-surface-check 0.2.34 → 0.2.35

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  No-payment CLI for checking x402 launch surfaces before a real agent spends.
4
4
 
5
- It accepts an x402 manifest or OpenAPI URL, derives public endpoints, sends no-payment probes, checks browser preflight behavior, and returns a Markdown patch queue. It never sends `X-PAYMENT`, never signs, and never attempts a paid call.
5
+ It accepts an x402 manifest or OpenAPI URL, derives public endpoints, sends no-payment probes, checks browser preflight behavior, and returns a Markdown patch queue. It never sends payment retry headers, never signs, and never attempts a paid call.
6
6
 
7
7
  npm: https://www.npmjs.com/package/x402-surface-check
8
8
  Attack-map field note: https://tateprograms.com/x402-attack-map-2026.html
@@ -38,7 +38,7 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
38
38
  - HTTPS resource URLs and stable resource metadata
39
39
  - Resource binding across top-level `resource.url` and every accept leg, including localhost/private-development resource URLs that should not ship in production
40
40
  - Timeout/expiry metadata on challenges, so payment capabilities have an explicit bounded freshness window
41
- - Browser CORS allowance for the requesting origin, `X-PAYMENT`, and exposed challenge/session headers on the actual 402 response
41
+ - Browser CORS allowance for the requesting origin, common x402/MPP retry headers, and exposed challenge/session headers on the actual 402 response
42
42
  - Cache-Control posture on no-payment challenge responses, with P1 warnings for explicitly cacheable payment gates and optional strict-cache findings for missing policy headers
43
43
  - Optional strict proof/idempotency posture: mutating paid routes that do not advertise payment-identifier idempotency, and payment challenges that do not advertise signed offer/receipt evidence
44
44
  - Payment-enforcement headers on `200` responses, so public telemetry/free-trial endpoints do not accidentally advertise enforced x402 while returning content before a challenge
@@ -785,7 +785,7 @@ async function probePreflight(entry, origin) {
785
785
  headers: {
786
786
  origin,
787
787
  'access-control-request-method': entry.method ?? 'POST',
788
- 'access-control-request-headers': 'content-type,x-payment',
788
+ 'access-control-request-headers': 'content-type,x-payment,payment-signature,authorization',
789
789
  },
790
790
  })
791
791
 
@@ -1070,6 +1070,24 @@ function missingExposedPaymentHeaders(result) {
1070
1070
  return critical.filter(name => !headerListAllows(expose, name))
1071
1071
  }
1072
1072
 
1073
+ function hasX402RetryChallenge(result) {
1074
+ if (['payment-required', 'x-payment-required'].includes(result.challengeHeaderName)) return true
1075
+ return challengeAccepts(result).some(accept => {
1076
+ const scheme = String(accept.scheme ?? '').toLowerCase()
1077
+ return scheme === 'exact' || scheme.includes('x402')
1078
+ })
1079
+ }
1080
+
1081
+ function hasMppRetryChallenge(result) {
1082
+ const authenticate = String(result.headers?.['www-authenticate'] ?? '')
1083
+ if (/^\s*payment\b/i.test(authenticate)) return true
1084
+ return challengeAccepts(result).some(accept => String(accept.scheme ?? '').toLowerCase() === 'mpp')
1085
+ }
1086
+
1087
+ function allowsAnyHeader(headerValue = '', names = []) {
1088
+ return names.some(name => headerListAllows(headerValue, name))
1089
+ }
1090
+
1073
1091
  function entryKey(entry) {
1074
1092
  return `${entry.method ?? 'POST'} ${entry.url}`
1075
1093
  }
@@ -1226,11 +1244,17 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1226
1244
  findings.push(`P1 - ${result.name} CORS preflight does not allow the requesting origin; observed allow-origin: none.`)
1227
1245
  }
1228
1246
  const allowed = result.headers['access-control-allow-headers'] ?? ''
1229
- if (allowed !== '*' && !/x-payment/i.test(allowed)) {
1247
+ if (hasX402RetryChallenge(challengeResult) && !allowsAnyHeader(allowed, ['x-payment', 'payment-signature'])) {
1248
+ const observed = result.status >= 400
1249
+ ? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
1250
+ : `allow headers: ${allowed || 'none'}`
1251
+ findings.push(`P1 - ${result.name} CORS preflight does not allow a known x402 retry header (X-PAYMENT or PAYMENT-SIGNATURE); observed ${observed}.`)
1252
+ }
1253
+ if (hasMppRetryChallenge(challengeResult) && !headerListAllows(allowed, 'authorization')) {
1230
1254
  const observed = result.status >= 400
1231
1255
  ? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
1232
1256
  : `allow headers: ${allowed || 'none'}`
1233
- findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed ${observed}.`)
1257
+ findings.push(`P1 - ${result.name} CORS preflight does not allow Authorization for MPP retry; observed ${observed}.`)
1234
1258
  }
1235
1259
  const allowedMethods = result.headers['access-control-allow-methods'] ?? ''
1236
1260
  if (/delete|put|patch/i.test(allowedMethods)) {
@@ -1256,8 +1280,11 @@ function groupedFindingLabel(finding) {
1256
1280
  if (/CORS preflight does not allow the requesting origin/.test(finding)) {
1257
1281
  return 'P1 - CORS preflight does not allow the requesting origin.'
1258
1282
  }
1259
- if (/CORS preflight does not allow X-PAYMENT/.test(finding)) {
1260
- return 'P1 - CORS preflight does not allow X-PAYMENT.'
1283
+ if (/CORS preflight does not allow a known x402 retry header/.test(finding)) {
1284
+ return 'P1 - CORS preflight does not allow a known x402 retry header.'
1285
+ }
1286
+ if (/CORS preflight does not allow Authorization for MPP retry/.test(finding)) {
1287
+ return 'P1 - CORS preflight does not allow Authorization for MPP retry.'
1261
1288
  }
1262
1289
  if (/challenge does not expose a signed\/intended resource URL|challenge does not repeat the resource URL|challenge resource URL differs/.test(finding)) {
1263
1290
  return 'P2 - Challenges have incomplete or inconsistent resource binding.'
@@ -1312,7 +1339,7 @@ function referenceGuides(findings) {
1312
1339
  if (!guides.some(guide => guide.url === url)) guides.push({ label, url })
1313
1340
  }
1314
1341
  const text = findings.join('\n')
1315
- if (/CORS|402 challenge response does not allow the requesting origin|X-PAYMENT/i.test(text)) {
1342
+ if (/CORS|402 challenge response does not allow the requesting origin|X-PAYMENT|PAYMENT-SIGNATURE|MPP retry/i.test(text)) {
1316
1343
  add('x402 CORS Fix', 'https://tateprograms.com/x402-cors-fix.html')
1317
1344
  add('Cloudflare x402 Worker Starter', 'https://tateprograms.com/cloudflare-x402-worker.html')
1318
1345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {