x402-surface-check 0.2.37 → 0.2.38

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
@@ -40,6 +40,7 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
40
40
  - HTTPS resource URLs and stable resource metadata
41
41
  - Resource binding across top-level `resource.url` and every accept leg, including localhost/private-development resource URLs that should not ship in production
42
42
  - Timeout/expiry metadata on challenges, so payment capabilities have an explicit bounded freshness window
43
+ - Payment-metadata privacy checks for sensitive resource query context, email/SSN/token-like values, prompt/private-context strings, and credential-like URLs in body or header-carried challenges
43
44
  - Browser CORS allowance for the requesting origin, common x402/MPP retry headers, and exposed challenge/session headers on the actual 402 response
44
45
  - 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
45
46
  - 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
@@ -1051,6 +1051,68 @@ function publicUrlCredentialFindings(value, path = 'document', depth = 0) {
1051
1051
  return []
1052
1052
  }
1053
1053
 
1054
+ const sensitiveQueryParamPattern =
1055
+ /^(?:account|account[_-]?id|address|birth[_-]?date|customer|customer[_-]?id|dob|email|e[_-]?mail|full[_-]?name|message|name|phone|postal|prompt|reason|session|session[_-]?id|ssn|tax[_-]?id|user|user[_-]?id|zip)$/i
1056
+
1057
+ function redactedSensitiveMetadataUrl(value) {
1058
+ if (!/^https?:\/\//i.test(String(value ?? ''))) return null
1059
+ try {
1060
+ const url = new URL(value)
1061
+ let changed = false
1062
+
1063
+ for (const [name] of Array.from(url.searchParams.entries())) {
1064
+ if (sensitiveQueryParamPattern.test(name)) {
1065
+ url.searchParams.set(name, 'REDACTED')
1066
+ changed = true
1067
+ }
1068
+ }
1069
+
1070
+ return changed ? url.toString() : null
1071
+ }
1072
+ catch {
1073
+ return null
1074
+ }
1075
+ }
1076
+
1077
+ function looksLikeSensitiveMetadataText(value) {
1078
+ const text = String(value ?? '')
1079
+ if (!text) return ''
1080
+ if (/\b\d{3}-\d{2}-\d{4}\b/.test(text)) return 'SSN-like value'
1081
+ if (/\b[A-Z0-9._%+-]+@(?!example\.com\b)(?!example\.org\b)(?!example\.net\b)[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(text)) return 'email address'
1082
+ if (/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/.test(text)) return 'JWT-like token'
1083
+ if (/\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{16,})\b/.test(text)) return 'API token-like value'
1084
+ if (/\b(?:prompt|user message|customer note|private context)\s*[:=]\s*\S+/i.test(text)) return 'prompt or private-context text'
1085
+ return ''
1086
+ }
1087
+
1088
+ function metadataPrivacyFindings(value, path = 'challenge', depth = 0) {
1089
+ if (depth > 8 || value === null || value === undefined) return []
1090
+ if (typeof value === 'string') {
1091
+ const credentialUrl = redactedCredentialUrl(value)
1092
+ if (credentialUrl) {
1093
+ return [`P2 - Payment metadata exposes credential-like URL material at ${path}: ${credentialUrl}. Move provider tokens, signatures, sessions, or API keys out of payment-visible metadata.`]
1094
+ }
1095
+ const sensitiveUrl = redactedSensitiveMetadataUrl(value)
1096
+ if (sensitiveUrl) {
1097
+ return [`P2 - Payment metadata exposes sensitive query context at ${path}: ${sensitiveUrl}. Keep resource URLs coarse and move private user context into server-side records.`]
1098
+ }
1099
+ const sensitiveKind = looksLikeSensitiveMetadataText(value)
1100
+ return sensitiveKind
1101
+ ? [`P2 - Payment metadata includes ${sensitiveKind} at ${path}. Redact or replace private context before it reaches facilitators, providers, logs, or receipts.`]
1102
+ : []
1103
+ }
1104
+ if (Array.isArray(value)) {
1105
+ return value.flatMap((item, index) => metadataPrivacyFindings(item, `${path}[${index}]`, depth + 1))
1106
+ }
1107
+ if (typeof value === 'object') {
1108
+ return Object.entries(value).flatMap(([key, item]) => {
1109
+ const safeKey = /^[a-zA-Z_$][\w$-]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
1110
+ return metadataPrivacyFindings(item, `${path}${safeKey}`, depth + 1)
1111
+ })
1112
+ }
1113
+ return []
1114
+ }
1115
+
1054
1116
  function cachePolicy(headers = {}) {
1055
1117
  return headers['cache-control'] ?? headers.cacheControl ?? ''
1056
1118
  }
@@ -1222,11 +1284,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1222
1284
  }
1223
1285
  }
1224
1286
  if (summary.resourceUrl.startsWith('http://') || summary.extraResource.startsWith('http://')) {
1225
- findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${summary.resourceUrl || summary.extraResource}.`)
1287
+ findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${displayPaymentMetadataValue(summary.resourceUrl || summary.extraResource)}.`)
1226
1288
  }
1227
1289
  if (looksLikeLocalResourceUrl(summary.resourceUrl) || looksLikeLocalResourceUrl(summary.extraResource)) {
1228
- findings.push(`P1 - ${result.name} challenge binds payment to a localhost/private-development resource URL: ${summary.resourceUrl || summary.extraResource}.`)
1290
+ findings.push(`P1 - ${result.name} challenge binds payment to a localhost/private-development resource URL: ${displayPaymentMetadataValue(summary.resourceUrl || summary.extraResource)}.`)
1229
1291
  }
1292
+ findings.push(...metadataPrivacyFindings(result.body.json, `${result.name} challenge`))
1230
1293
  if (!summary.amount || !summary.payTo || !summary.asset) {
1231
1294
  findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
1232
1295
  }
@@ -1358,6 +1421,9 @@ function groupedFindingLabel(finding) {
1358
1421
  if (/does not advertise signed offer\/receipt metadata/.test(finding)) {
1359
1422
  return 'P3 - Payment challenges do not advertise signed offer/receipt metadata.'
1360
1423
  }
1424
+ if (/Payment metadata exposes|Payment metadata includes/.test(finding)) {
1425
+ return 'P2 - Payment metadata exposes private or sensitive context.'
1426
+ }
1361
1427
  if (/content while payment headers advertise enforcement/.test(finding)) {
1362
1428
  return 'P2 - Payment headers advertise enforcement on a 200 response.'
1363
1429
  }
@@ -1402,13 +1468,21 @@ function referenceGuides(findings) {
1402
1468
  add('x402 Signed Offers & Receipts', 'https://docs.x402.org/extensions/offer-receipt')
1403
1469
  add('x402 Attack Map 2026', 'https://tateprograms.com/x402-attack-map-2026.html')
1404
1470
  }
1405
- if (/credential-like URL material|provider tokens|API keys|registry-visible endpoint URLs/i.test(text)) {
1471
+ if (/credential-like URL material|provider tokens|API keys|registry-visible endpoint URLs|Payment metadata exposes|Payment metadata includes|private user context|facilitators, providers, logs, or receipts/i.test(text)) {
1406
1472
  add('x402 Metadata Filter', 'https://tateprograms.com/x402-metadata-filter.html')
1407
1473
  add('Agent Commerce Gate', 'https://tateprograms.com/agent-commerce-gate.html')
1408
1474
  }
1409
1475
  return guides.map(guide => `- ${guide.label}: ${guide.url}`)
1410
1476
  }
1411
1477
 
1478
+ function displayPaymentMetadataValue(value) {
1479
+ if (value === null || value === undefined || value === '') return '-'
1480
+ if (typeof value !== 'string') return displayMetadataValue(value)
1481
+ return redactedCredentialUrl(value)
1482
+ ?? redactedSensitiveMetadataUrl(value)
1483
+ ?? value
1484
+ }
1485
+
1412
1486
  function formatMarkdown(report) {
1413
1487
  const document = report.document.body.json ?? {}
1414
1488
  const documentType = report.directEndpoint
@@ -1416,7 +1490,7 @@ function formatMarkdown(report) {
1416
1490
  : (report.mcpCatalog?.tools?.length ? 'Streamable HTTP MCP endpoint' : (document.openapi ? 'OpenAPI' : 'x402 manifest or JSON document'))
1417
1491
  const challengeRows = report.challenges.map(result => {
1418
1492
  const summary = challengeSummary(result)
1419
- return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${summary.protocol || '-'} | ${summary.price || '-'} | ${summary.network || '-'} | ${summary.resourceUrl || '-'} |`
1493
+ return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${summary.protocol || '-'} | ${summary.price || '-'} | ${summary.network || '-'} | ${displayPaymentMetadataValue(summary.resourceUrl)} |`
1420
1494
  })
1421
1495
  const preflightRows = report.preflights.map(result => {
1422
1496
  return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${result.headers['access-control-allow-origin'] ?? '-'} | ${result.headers['access-control-allow-headers'] ?? '-'} | ${result.headers['access-control-allow-methods'] ?? '-'} |`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.37",
3
+ "version": "0.2.38",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {