x402-surface-check 0.2.39 → 0.2.41

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[]`, 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
23
+ - Manifest endpoint discovery from `items[]`, `endpoints[]`, marketplace `skills[]` / `catalog.skills[]`, `agents[].tools[]` resource URLs, 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
@@ -38,7 +38,7 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
38
38
  - Placeholder recipients such as zero addresses and Solana system-program values
39
39
  - Testnet or staging rails such as Base Sepolia and Solana devnet
40
40
  - HTTPS resource URLs and stable resource metadata
41
- - Resource binding across top-level `resource.url` and every accept leg, including localhost/private-development resource URLs that should not ship in production
41
+ - Resource binding across top-level `resource.url`; legacy/v1 accept-leg resource echoes; and 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
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
44
44
  - Browser CORS allowance for the requesting origin, common x402/MPP retry headers, and exposed challenge/session headers on the actual 402 response
@@ -376,6 +376,22 @@ function marketplaceSkillPriceUsd(skill) {
376
376
  return numberFromDecimal(value)
377
377
  }
378
378
 
379
+ function endpointRawPath(endpoint) {
380
+ if (!endpoint || typeof endpoint !== 'object') return undefined
381
+ const values = [
382
+ endpoint.url,
383
+ endpoint.endpoint,
384
+ endpoint.resourceUrl,
385
+ endpoint.resourceURL,
386
+ endpoint.resource_url,
387
+ endpoint.resource?.url,
388
+ endpoint.resource?.uri,
389
+ endpoint.resource,
390
+ endpoint.path,
391
+ ]
392
+ return values.find(value => typeof value === 'string' && value.trim())
393
+ }
394
+
379
395
  function manifestEndpointUrl(rawPath, endpoint, baseUrl, sourceUrl) {
380
396
  const url = new URL(endpointUrl(rawPath, baseUrl, sourceUrl))
381
397
  const parameters = endpoint?.parameters
@@ -432,7 +448,7 @@ function endpointEntries(document, sourceUrl, limit) {
432
448
  for (const skillList of marketplaceSkillLists) {
433
449
  for (const skill of skillList) {
434
450
  if (!skill || typeof skill !== 'object') continue
435
- const rawPath = skill.endpoint ?? skill.url ?? skill.path
451
+ const rawPath = endpointRawPath(skill)
436
452
  if (!rawPath) continue
437
453
  if (redactedCredentialUrl(rawPath)) continue
438
454
  entries.push({
@@ -445,9 +461,41 @@ function endpointEntries(document, sourceUrl, limit) {
445
461
  }
446
462
  }
447
463
 
464
+ const agentCatalogLists = [
465
+ document.agents,
466
+ document.catalog?.agents,
467
+ document.marketplace?.agents,
468
+ ].filter(Array.isArray)
469
+
470
+ for (const agentList of agentCatalogLists) {
471
+ for (const agent of agentList) {
472
+ if (!agent || typeof agent !== 'object' || !Array.isArray(agent.tools)) continue
473
+ const agentName = agent.agentId ?? agent.agent_id ?? agent.slug ?? agent.id ?? agent.name
474
+ for (const tool of agent.tools) {
475
+ if (!tool || typeof tool !== 'object') continue
476
+ const rawPath = endpointRawPath(tool)
477
+ if (!rawPath) continue
478
+ if (redactedCredentialUrl(rawPath)) continue
479
+ const method = String(tool.method ?? 'POST').toUpperCase()
480
+ const paymentSignal = Math.max(manifestEndpointPaymentSignal(agent), manifestEndpointPaymentSignal(tool))
481
+ const hasPathParameters = /\{[^}]+\}/.test(String(rawPath))
482
+ if (paymentSignal === 0 && (method !== 'GET' || hasPathParameters)) continue
483
+ const toolName = tool.slug ?? tool.id ?? tool.name ?? String(rawPath).split('/').filter(Boolean).at(-1)
484
+ entries.push({
485
+ name: agentName && toolName ? `${agentName}/${toolName}` : toolName ?? agentName ?? String(rawPath),
486
+ url: manifestEndpointUrl(rawPath, tool, baseUrl, sourceUrl),
487
+ method,
488
+ expectedPriceUsd: marketplaceSkillPriceUsd(tool) ?? marketplaceSkillPriceUsd(agent),
489
+ requestBody: marketplaceSkillBody(tool) ?? marketplaceSkillBody(agent),
490
+ publicDiscovery: paymentSignal === 0,
491
+ })
492
+ }
493
+ }
494
+ }
495
+
448
496
  if (Array.isArray(document.endpoints)) {
449
497
  for (const endpoint of document.endpoints) {
450
- const rawPath = endpoint?.url ?? endpoint?.endpoint ?? endpoint?.path
498
+ const rawPath = endpointRawPath(endpoint)
451
499
  if (!rawPath) continue
452
500
  entries.push({
453
501
  name: endpoint.id ?? endpoint.name ?? String(rawPath).split('/').filter(Boolean).at(-1) ?? String(rawPath),
@@ -487,7 +535,7 @@ function endpointEntries(document, sourceUrl, limit) {
487
535
  if (Array.isArray(document.tools)) {
488
536
  for (const tool of document.tools) {
489
537
  if (!tool || typeof tool !== 'object') continue
490
- const rawPath = tool.url ?? tool.endpoint ?? tool.path
538
+ const rawPath = endpointRawPath(tool)
491
539
  if (!rawPath) continue
492
540
  const method = String(tool.method ?? 'POST').toUpperCase()
493
541
  const paymentSignal = manifestEndpointPaymentSignal(tool)
@@ -516,7 +564,7 @@ function endpointEntries(document, sourceUrl, limit) {
516
564
  }
517
565
  if (!endpoint || typeof endpoint !== 'object') continue
518
566
  const keyPath = key.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)/i)?.[2] ?? key
519
- const rawPath = endpoint.url ?? endpoint.endpoint ?? endpoint.path ?? keyPath
567
+ const rawPath = endpointRawPath(endpoint) ?? keyPath
520
568
  if (!rawPath) continue
521
569
  const method = String(endpoint.method ?? 'POST').toUpperCase()
522
570
  const paymentSignal = manifestEndpointPaymentSignal(endpoint)
@@ -535,7 +583,7 @@ function endpointEntries(document, sourceUrl, limit) {
535
583
  if (Array.isArray(document.items)) {
536
584
  for (const item of document.items) {
537
585
  if (item?.type && item.type !== 'http') continue
538
- const rawPath = item?.resource ?? item?.url ?? item?.endpoint ?? item?.path
586
+ const rawPath = endpointRawPath(item)
539
587
  if (!rawPath) continue
540
588
  entries.push({
541
589
  name: item.metadata?.name ?? item.id ?? item.name ?? String(rawPath).split('/').filter(Boolean).at(-1) ?? String(rawPath),
@@ -588,7 +636,7 @@ function endpointEntries(document, sourceUrl, limit) {
588
636
  }
589
637
 
590
638
  if (!resource || typeof resource !== 'object') continue
591
- const rawPath = resource.url ?? resource.endpoint ?? resource.resource ?? resource.path
639
+ const rawPath = endpointRawPath(resource)
592
640
  if (!rawPath) continue
593
641
  entries.push({
594
642
  name: resource.id
@@ -945,6 +993,10 @@ function challengeResourceValue(challenge) {
945
993
  ?? ''
946
994
  }
947
995
 
996
+ function isX402V2Challenge(challenge) {
997
+ return Number(challenge?.x402Version) === 2
998
+ }
999
+
948
1000
  function hasFreshnessMetadata(challenge, accept) {
949
1001
  return [
950
1002
  challenge?.expires,
@@ -1356,7 +1408,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
1356
1408
  if (!topResource && populatedAcceptResources.length === 0) {
1357
1409
  findings.push(`P2 - ${result.name} challenge does not expose a signed/intended resource URL at the top level or in any accept leg.`)
1358
1410
  }
1359
- else if (accepts.length > 0 && populatedAcceptResources.length < accepts.length) {
1411
+ else if (!isX402V2Challenge(result.body.json) && accepts.length > 0 && populatedAcceptResources.length < accepts.length) {
1360
1412
  findings.push(`P2 - ${result.name} challenge does not repeat the resource URL in every accept leg for spend-map and replay binding.`)
1361
1413
  }
1362
1414
  if (topResource && populatedAcceptResources.some(resource => resource !== topResource)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {