x402-surface-check 0.2.1 → 0.2.3

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
@@ -14,10 +14,10 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
14
14
 
15
15
  ## What It Checks
16
16
 
17
- - Manifest endpoint discovery from `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
17
+ - Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
18
18
  - No-payment HTTP 402 challenge shape
19
19
  - x402 v1 and v2 price fields
20
- - MPP `WWW-Authenticate: Payment` challenges
20
+ - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
21
21
  - `amount` / `maxAmountRequired`, `asset`, `network`, and `payTo`
22
22
  - Placeholder recipients such as zero addresses and Solana system-program values
23
23
  - Testnet or staging rails such as Base Sepolia and Solana devnet
@@ -151,6 +151,19 @@ function endpointEntries(document, sourceUrl, limit) {
151
151
  }
152
152
  }
153
153
 
154
+ if (Array.isArray(document.items)) {
155
+ for (const item of document.items) {
156
+ if (item?.type && item.type !== 'http') continue
157
+ const rawPath = item?.resource ?? item?.url ?? item?.endpoint ?? item?.path
158
+ if (!rawPath) continue
159
+ entries.push({
160
+ name: item.metadata?.name ?? item.id ?? item.name ?? String(rawPath).split('/').filter(Boolean).at(-1) ?? String(rawPath),
161
+ url: endpointUrl(rawPath, baseUrl, sourceUrl),
162
+ method: String(item.method ?? 'GET').toUpperCase(),
163
+ })
164
+ }
165
+ }
166
+
154
167
  if (document.openapi && document.paths && typeof document.paths === 'object') {
155
168
  const baseUrl = document.servers?.find(server => typeof server?.url === 'string')?.url
156
169
  ?? sourceUrl
@@ -215,17 +228,25 @@ function parseEncodedChallenge(value) {
215
228
  }
216
229
  }
217
230
 
218
- function parsePaymentAuthenticate(value) {
219
- if (!value || !/^Payment\s+/i.test(value)) return null
231
+ function authenticateParams(value, scheme) {
232
+ const header = String(value ?? '').replace(/^www-authenticate:\s*/i, '').trim()
233
+ if (!header || !new RegExp(`^${scheme}\\s+`, 'i').test(header)) return null
220
234
  const params = {}
221
235
  const pattern = /([a-zA-Z][\w-]*)="([^"]*)"/g
222
- let match = pattern.exec(value)
236
+ let match = pattern.exec(header)
223
237
 
224
238
  while (match) {
225
239
  params[match[1]] = match[2]
226
- match = pattern.exec(value)
240
+ match = pattern.exec(header)
227
241
  }
228
242
 
243
+ return params
244
+ }
245
+
246
+ function parsePaymentAuthenticate(value) {
247
+ const params = authenticateParams(value, 'Payment')
248
+ if (!params) return null
249
+
229
250
  const request = parseEncodedChallenge(params.request)
230
251
  if (!request) return null
231
252
 
@@ -251,6 +272,19 @@ function parsePaymentAuthenticate(value) {
251
272
  }
252
273
  }
253
274
 
275
+ function parseX402Authenticate(value) {
276
+ const params = authenticateParams(value, 'X402')
277
+ if (!params) return null
278
+
279
+ const requirements = parseEncodedChallenge(params.requirements ?? params.request)
280
+ if (!requirements || !Array.isArray(requirements.accepts)) return null
281
+
282
+ return {
283
+ protocol: requirements.protocol ?? 'x402',
284
+ ...requirements,
285
+ }
286
+ }
287
+
254
288
  async function fetchDocument(url) {
255
289
  const response = await fetch(url, {
256
290
  headers: {
@@ -283,16 +317,18 @@ async function probeEndpoint(entry) {
283
317
  const headerChallenge = parseEncodedChallenge(
284
318
  response.headers.get('payment-required') ?? response.headers.get('x-payment-required'),
285
319
  )
286
- const paymentChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
320
+ const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
321
+ ?? parseX402Authenticate(response.headers.get('www-authenticate'))
287
322
 
288
323
  if (!body.json?.accepts?.length) {
289
324
  if (headerChallenge) {
290
325
  body.json = headerChallenge
291
326
  }
292
- else if (paymentChallenge) {
293
- paymentChallenge.resource.url = entry.url
294
- paymentChallenge.accepts[0].resource = entry.url
295
- body.json = paymentChallenge
327
+ else if (authenticateChallenge) {
328
+ authenticateChallenge.resource = authenticateChallenge.resource ?? { url: entry.url }
329
+ authenticateChallenge.resource.url = authenticateChallenge.resource.url || entry.url
330
+ authenticateChallenge.accepts[0].resource = authenticateChallenge.accepts[0].resource || entry.url
331
+ body.json = authenticateChallenge
296
332
  }
297
333
  }
298
334
 
@@ -385,7 +421,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
385
421
  }
386
422
 
387
423
  if (entries.length === 0) {
388
- findings.push('P1 - Document does not expose any manifest, OpenAPI, category, or resource endpoints for no-payment probes.')
424
+ findings.push('P1 - Document does not expose any manifest, OpenAPI, item, category, or resource endpoints for no-payment probes.')
389
425
  }
390
426
 
391
427
  for (const result of challengeResults) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {