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 +2 -2
- package/bin/x402-surface-check.mjs +59 -7
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)) {
|