x402-surface-check 0.2.10 → 0.2.12

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
@@ -15,6 +15,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
15
15
  ## What It Checks
16
16
 
17
17
  - Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
18
+ - Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, or `resourcesUrl`
18
19
  - No-payment HTTP 402 challenge shape
19
20
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
20
21
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
@@ -27,6 +28,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
27
28
  - Browser CORS allowance for `X-PAYMENT`
28
29
  - Over-broad public method surfaces
29
30
  - Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
31
+ - Operational health/status endpoints, without treating expected free health checks as paid-route failures
30
32
  - Object-valued document metadata such as facilitator objects, without `[object Object]` report artifacts
31
33
 
32
34
  ## Public Proof
@@ -36,6 +38,9 @@ Recent public no-payment checks have found and verified real launch fixes:
36
38
  - TensorFeed: parameter-required premium routes moved behind canonical x402 V2 challenges, then verified clean. https://github.com/solana-foundation/pay-skills/pull/68#issuecomment-4455360068
37
39
  - x402jp: weather routes that returned 500 now return structured Base x402 challenges. https://github.com/solana-foundation/pay-skills/pull/58#issuecomment-4455401355
38
40
  - Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
41
+ - UZPROOF: schemes-style Solana x402 challenge and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/28#issuecomment-4455613892
42
+ - HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
43
+ - anchor-x402: multi-rail x402 challenges verified, with browser preflight blockers isolated before merge. https://github.com/solana-foundation/pay-skills/pull/47#issuecomment-4455678163
39
44
  - Agent Trust Bench: live discovery URL and browser-compatibility notes for adversarial agent-payment resources. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4455484414
40
45
 
41
46
  Field notes and browser version: https://tateprograms.com/x402-surface-check.html
@@ -132,6 +132,15 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
132
132
  return new URL(value, base).toString()
133
133
  }
134
134
 
135
+ function linkedDiscoveryUrl(document, sourceUrl) {
136
+ const rawUrl = document?.discovery_url
137
+ ?? document?.discoveryUrl
138
+ ?? document?.resources_url
139
+ ?? document?.resourcesUrl
140
+ if (typeof rawUrl !== 'string' || !rawUrl.trim()) return ''
141
+ return endpointUrl(rawUrl, documentBaseUrl(document, sourceUrl), sourceUrl)
142
+ }
143
+
135
144
  function operationExpectedPrice(operation) {
136
145
  const price = operation?.['x-payment-info']?.price
137
146
  ?? operation?.['x-payment']?.price
@@ -501,11 +510,21 @@ function looksLikePlaceholderPayTo(payTo) {
501
510
  return false
502
511
  }
503
512
 
513
+ function entryKey(entry) {
514
+ return `${entry.method ?? 'POST'} ${entry.url}`
515
+ }
516
+
517
+ function looksLikeOperationalHealthEndpoint(result) {
518
+ const value = `${result.name ?? ''} ${new URL(result.url).pathname}`.toLowerCase()
519
+ return /(^|[/_\s-])(health|healthz|ready|readiness|live|liveness|status)([/_\s-]|$)/.test(value)
520
+ }
521
+
504
522
  function findingList(documentResult, challengeResults, preflightResults, entries) {
505
523
  const document = documentResult.body.json ?? {}
506
524
  const findings = []
507
525
  const networks = valueList(document.networks)
508
526
  const challengeNetworks = new Set()
527
+ const challengesByEntry = new Map(challengeResults.map(result => [entryKey(result), result]))
509
528
 
510
529
  if (documentResult.status < 200 || documentResult.status >= 300) {
511
530
  findings.push(`P1 - Document returned HTTP ${documentResult.status}; expected a successful JSON response.`)
@@ -526,7 +545,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
526
545
 
527
546
  if (result.status !== 402) {
528
547
  if (result.status >= 200 && result.status < 300) {
529
- findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
548
+ if (!looksLikeOperationalHealthEndpoint(result)) {
549
+ findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
550
+ }
530
551
  }
531
552
  else if (result.status === 400 || result.status === 422) {
532
553
  findings.push(`P1 - ${result.name} returned validation HTTP ${result.status} before a payment challenge for a no-payment ${result.method ?? 'POST'} probe.`)
@@ -569,9 +590,14 @@ function findingList(documentResult, challengeResults, preflightResults, entries
569
590
  }
570
591
 
571
592
  for (const result of preflightResults) {
593
+ const challengeResult = challengesByEntry.get(entryKey(result))
594
+ if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
572
595
  const allowed = result.headers['access-control-allow-headers'] ?? ''
573
596
  if (allowed !== '*' && !/x-payment/i.test(allowed)) {
574
- findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed allow headers: ${allowed || 'none'}.`)
597
+ const observed = result.status >= 400
598
+ ? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
599
+ : `allow headers: ${allowed || 'none'}`
600
+ findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed ${observed}.`)
575
601
  }
576
602
  const allowedMethods = result.headers['access-control-allow-methods'] ?? ''
577
603
  if (/delete|put|patch/i.test(allowedMethods)) {
@@ -603,6 +629,7 @@ function formatMarkdown(report) {
603
629
  return [
604
630
  '# x402 Public Surface Check',
605
631
  '',
632
+ ...(report.sourceDocument ? [`Source: ${report.sourceDocument.url}`] : []),
606
633
  `Document: ${report.document.url}`,
607
634
  `Checked: ${report.checkedAt}`,
608
635
  'Scope: manifest/OpenAPI parsing, no-payment endpoint probes, and browser-style CORS preflight. No payment headers or paid calls.',
@@ -639,7 +666,8 @@ function formatMarkdown(report) {
639
666
  }
640
667
 
641
668
  async function runCheck(options) {
642
- const document = options.endpoint
669
+ let sourceDocument = null
670
+ let document = options.endpoint
643
671
  ? {
644
672
  status: 200,
645
673
  ok: true,
@@ -648,9 +676,25 @@ async function runCheck(options) {
648
676
  body: { text: '{}', json: {} },
649
677
  }
650
678
  : await fetchDocument(options.url)
651
- const entries = options.endpoint
679
+ let entries = options.endpoint
652
680
  ? [{ name: new URL(options.url).pathname.split('/').filter(Boolean).at(-1) ?? options.url, url: options.url, method: options.method || 'POST' }]
653
681
  : (document.body.json ? endpointEntries(document.body.json, document.url, options.limit) : [])
682
+
683
+ if (!options.endpoint && entries.length === 0 && document.body.json) {
684
+ const discoveryUrl = linkedDiscoveryUrl(document.body.json, document.url)
685
+ if (discoveryUrl) {
686
+ const followedDocument = await fetchDocument(discoveryUrl)
687
+ const followedEntries = followedDocument.body.json
688
+ ? endpointEntries(followedDocument.body.json, followedDocument.url, options.limit)
689
+ : []
690
+ if (followedEntries.length > 0) {
691
+ sourceDocument = document
692
+ document = followedDocument
693
+ entries = followedEntries
694
+ }
695
+ }
696
+ }
697
+
654
698
  const origin = options.origin ?? new URL(document.url).origin
655
699
  const challenges = []
656
700
  const preflights = []
@@ -669,6 +713,7 @@ async function runCheck(options) {
669
713
  origin,
670
714
  challenges,
671
715
  preflights,
716
+ sourceDocument,
672
717
  }
673
718
  report.findings = findingList(document, challenges, preflights, entries)
674
719
  return report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {