x402-surface-check 0.2.37 → 0.2.39

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
@@ -20,7 +20,7 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
20
20
 
21
21
  ## What It Checks
22
22
 
23
- - Manifest endpoint discovery from `items[]`, `endpoints[]`, object-valued `endpoints`, string-valued endpoint maps, `tools` maps, `resources[]`, `x402Endpoints`, category arrays, raw resource URL strings, method-prefixed resource strings, and OpenAPI paths
23
+ - Manifest endpoint discovery from `items[]`, `endpoints[]`, marketplace `skills[]` / `catalog.skills[]`, object-valued `endpoints`, string-valued endpoint maps, `tools` maps, `resources[]`, `x402Endpoints`, category arrays, raw resource URL strings, method-prefixed resource strings, and OpenAPI paths
24
24
  - Streamable HTTP MCP tool catalogs via safe JSON-RPC `tools/list` probes with `Accept: application/json, text/event-stream`
25
25
  - Object-valued manifest endpoint query examples, public catalog/discovery GETs, and payment-bearing two-phase operations without treating expected public catalog reads as failed payment gates
26
26
  - Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, `resourcesUrl`, string `discovery` links, nested `discovery.x402_json` / OpenAPI links, or manifest-level OpenAPI links
@@ -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
@@ -358,6 +358,24 @@ function manifestEndpointBody(endpoint, document) {
358
358
  return exampleValue(body, document)
359
359
  }
360
360
 
361
+ function marketplaceSkillBody(skill) {
362
+ return skill?.example?.input
363
+ ?? skill?.exampleInput
364
+ ?? skill?.input?.example
365
+ ?? skill?.input?.safe_example
366
+ ?? skill?.input?.safeExample
367
+ ?? manifestEndpointBody(skill, {})
368
+ }
369
+
370
+ function marketplaceSkillPriceUsd(skill) {
371
+ const value = skill?.price?.amount
372
+ ?? skill?.price?.usd
373
+ ?? skill?.priceUsd
374
+ ?? skill?.price_usd
375
+ ?? skill?.pricePerCall
376
+ return numberFromDecimal(value)
377
+ }
378
+
361
379
  function manifestEndpointUrl(rawPath, endpoint, baseUrl, sourceUrl) {
362
380
  const url = new URL(endpointUrl(rawPath, baseUrl, sourceUrl))
363
381
  const parameters = endpoint?.parameters
@@ -404,6 +422,29 @@ function endpointEntries(document, sourceUrl, limit) {
404
422
  }
405
423
  }
406
424
 
425
+ const marketplaceSkillLists = [
426
+ document.skills,
427
+ document.services,
428
+ document.catalog?.skills,
429
+ document.catalog?.services,
430
+ ].filter(Array.isArray)
431
+
432
+ for (const skillList of marketplaceSkillLists) {
433
+ for (const skill of skillList) {
434
+ if (!skill || typeof skill !== 'object') continue
435
+ const rawPath = skill.endpoint ?? skill.url ?? skill.path
436
+ if (!rawPath) continue
437
+ if (redactedCredentialUrl(rawPath)) continue
438
+ entries.push({
439
+ name: skill.slug ?? skill.id ?? skill.name ?? String(rawPath).split('/').filter(Boolean).at(-1) ?? String(rawPath),
440
+ url: endpointUrl(rawPath, baseUrl, sourceUrl),
441
+ method: String(skill.method ?? 'POST').toUpperCase(),
442
+ expectedPriceUsd: marketplaceSkillPriceUsd(skill),
443
+ requestBody: marketplaceSkillBody(skill),
444
+ })
445
+ }
446
+ }
447
+
407
448
  if (Array.isArray(document.endpoints)) {
408
449
  for (const endpoint of document.endpoints) {
409
450
  const rawPath = endpoint?.url ?? endpoint?.endpoint ?? endpoint?.path
@@ -1051,6 +1092,68 @@ function publicUrlCredentialFindings(value, path = 'document', depth = 0) {
1051
1092
  return []
1052
1093
  }
1053
1094
 
1095
+ const sensitiveQueryParamPattern =
1096
+ /^(?: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
1097
+
1098
+ function redactedSensitiveMetadataUrl(value) {
1099
+ if (!/^https?:\/\//i.test(String(value ?? ''))) return null
1100
+ try {
1101
+ const url = new URL(value)
1102
+ let changed = false
1103
+
1104
+ for (const [name] of Array.from(url.searchParams.entries())) {
1105
+ if (sensitiveQueryParamPattern.test(name)) {
1106
+ url.searchParams.set(name, 'REDACTED')
1107
+ changed = true
1108
+ }
1109
+ }
1110
+
1111
+ return changed ? url.toString() : null
1112
+ }
1113
+ catch {
1114
+ return null
1115
+ }
1116
+ }
1117
+
1118
+ function looksLikeSensitiveMetadataText(value) {
1119
+ const text = String(value ?? '')
1120
+ if (!text) return ''
1121
+ if (/\b\d{3}-\d{2}-\d{4}\b/.test(text)) return 'SSN-like value'
1122
+ 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'
1123
+ if (/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/.test(text)) return 'JWT-like token'
1124
+ 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'
1125
+ if (/\b(?:prompt|user message|customer note|private context)\s*[:=]\s*\S+/i.test(text)) return 'prompt or private-context text'
1126
+ return ''
1127
+ }
1128
+
1129
+ function metadataPrivacyFindings(value, path = 'challenge', depth = 0) {
1130
+ if (depth > 8 || value === null || value === undefined) return []
1131
+ if (typeof value === 'string') {
1132
+ const credentialUrl = redactedCredentialUrl(value)
1133
+ if (credentialUrl) {
1134
+ 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.`]
1135
+ }
1136
+ const sensitiveUrl = redactedSensitiveMetadataUrl(value)
1137
+ if (sensitiveUrl) {
1138
+ return [`P2 - Payment metadata exposes sensitive query context at ${path}: ${sensitiveUrl}. Keep resource URLs coarse and move private user context into server-side records.`]
1139
+ }
1140
+ const sensitiveKind = looksLikeSensitiveMetadataText(value)
1141
+ return sensitiveKind
1142
+ ? [`P2 - Payment metadata includes ${sensitiveKind} at ${path}. Redact or replace private context before it reaches facilitators, providers, logs, or receipts.`]
1143
+ : []
1144
+ }
1145
+ if (Array.isArray(value)) {
1146
+ return value.flatMap((item, index) => metadataPrivacyFindings(item, `${path}[${index}]`, depth + 1))
1147
+ }
1148
+ if (typeof value === 'object') {
1149
+ return Object.entries(value).flatMap(([key, item]) => {
1150
+ const safeKey = /^[a-zA-Z_$][\w$-]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
1151
+ return metadataPrivacyFindings(item, `${path}${safeKey}`, depth + 1)
1152
+ })
1153
+ }
1154
+ return []
1155
+ }
1156
+
1054
1157
  function cachePolicy(headers = {}) {
1055
1158
  return headers['cache-control'] ?? headers.cacheControl ?? ''
1056
1159
  }
@@ -1222,11 +1325,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1222
1325
  }
1223
1326
  }
1224
1327
  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}.`)
1328
+ findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${displayPaymentMetadataValue(summary.resourceUrl || summary.extraResource)}.`)
1226
1329
  }
1227
1330
  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}.`)
1331
+ findings.push(`P1 - ${result.name} challenge binds payment to a localhost/private-development resource URL: ${displayPaymentMetadataValue(summary.resourceUrl || summary.extraResource)}.`)
1229
1332
  }
1333
+ findings.push(...metadataPrivacyFindings(result.body.json, `${result.name} challenge`))
1230
1334
  if (!summary.amount || !summary.payTo || !summary.asset) {
1231
1335
  findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
1232
1336
  }
@@ -1358,6 +1462,9 @@ function groupedFindingLabel(finding) {
1358
1462
  if (/does not advertise signed offer\/receipt metadata/.test(finding)) {
1359
1463
  return 'P3 - Payment challenges do not advertise signed offer/receipt metadata.'
1360
1464
  }
1465
+ if (/Payment metadata exposes|Payment metadata includes/.test(finding)) {
1466
+ return 'P2 - Payment metadata exposes private or sensitive context.'
1467
+ }
1361
1468
  if (/content while payment headers advertise enforcement/.test(finding)) {
1362
1469
  return 'P2 - Payment headers advertise enforcement on a 200 response.'
1363
1470
  }
@@ -1402,13 +1509,21 @@ function referenceGuides(findings) {
1402
1509
  add('x402 Signed Offers & Receipts', 'https://docs.x402.org/extensions/offer-receipt')
1403
1510
  add('x402 Attack Map 2026', 'https://tateprograms.com/x402-attack-map-2026.html')
1404
1511
  }
1405
- if (/credential-like URL material|provider tokens|API keys|registry-visible endpoint URLs/i.test(text)) {
1512
+ 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
1513
  add('x402 Metadata Filter', 'https://tateprograms.com/x402-metadata-filter.html')
1407
1514
  add('Agent Commerce Gate', 'https://tateprograms.com/agent-commerce-gate.html')
1408
1515
  }
1409
1516
  return guides.map(guide => `- ${guide.label}: ${guide.url}`)
1410
1517
  }
1411
1518
 
1519
+ function displayPaymentMetadataValue(value) {
1520
+ if (value === null || value === undefined || value === '') return '-'
1521
+ if (typeof value !== 'string') return displayMetadataValue(value)
1522
+ return redactedCredentialUrl(value)
1523
+ ?? redactedSensitiveMetadataUrl(value)
1524
+ ?? value
1525
+ }
1526
+
1412
1527
  function formatMarkdown(report) {
1413
1528
  const document = report.document.body.json ?? {}
1414
1529
  const documentType = report.directEndpoint
@@ -1416,7 +1531,7 @@ function formatMarkdown(report) {
1416
1531
  : (report.mcpCatalog?.tools?.length ? 'Streamable HTTP MCP endpoint' : (document.openapi ? 'OpenAPI' : 'x402 manifest or JSON document'))
1417
1532
  const challengeRows = report.challenges.map(result => {
1418
1533
  const summary = challengeSummary(result)
1419
- return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${summary.protocol || '-'} | ${summary.price || '-'} | ${summary.network || '-'} | ${summary.resourceUrl || '-'} |`
1534
+ return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${summary.protocol || '-'} | ${summary.price || '-'} | ${summary.network || '-'} | ${displayPaymentMetadataValue(summary.resourceUrl)} |`
1420
1535
  })
1421
1536
  const preflightRows = report.preflights.map(result => {
1422
1537
  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.39",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {