mppx 0.8.6 → 0.8.7

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli/internal.js +2 -2
  3. package/dist/cli/internal.js.map +1 -1
  4. package/dist/cli/plugins/evm.d.ts +2 -0
  5. package/dist/cli/plugins/evm.d.ts.map +1 -0
  6. package/dist/cli/plugins/evm.js +51 -0
  7. package/dist/cli/plugins/evm.js.map +1 -0
  8. package/dist/cli/plugins/index.d.ts +1 -0
  9. package/dist/cli/plugins/index.d.ts.map +1 -1
  10. package/dist/cli/plugins/index.js +1 -0
  11. package/dist/cli/plugins/index.js.map +1 -1
  12. package/dist/cli/validate/challenge.d.ts +2 -0
  13. package/dist/cli/validate/challenge.d.ts.map +1 -1
  14. package/dist/cli/validate/challenge.js +184 -78
  15. package/dist/cli/validate/challenge.js.map +1 -1
  16. package/dist/cli/validate/discovery.d.ts +1 -0
  17. package/dist/cli/validate/discovery.d.ts.map +1 -1
  18. package/dist/cli/validate/discovery.js +99 -35
  19. package/dist/cli/validate/discovery.js.map +1 -1
  20. package/dist/cli/validate/helpers.d.ts +14 -1
  21. package/dist/cli/validate/helpers.d.ts.map +1 -1
  22. package/dist/cli/validate/helpers.js +0 -13
  23. package/dist/cli/validate/helpers.js.map +1 -1
  24. package/dist/cli/validate/index.d.ts +1 -0
  25. package/dist/cli/validate/index.d.ts.map +1 -1
  26. package/dist/cli/validate/index.js +98 -151
  27. package/dist/cli/validate/index.js.map +1 -1
  28. package/dist/cli/validate/payment.d.ts +1 -0
  29. package/dist/cli/validate/payment.d.ts.map +1 -1
  30. package/dist/cli/validate/payment.js +215 -144
  31. package/dist/cli/validate/payment.js.map +1 -1
  32. package/dist/evm/Chains.d.ts +4 -0
  33. package/dist/evm/Chains.d.ts.map +1 -1
  34. package/dist/evm/Chains.js +4 -0
  35. package/dist/evm/Chains.js.map +1 -1
  36. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  37. package/dist/internal/AcceptPayment.js +7 -1
  38. package/dist/internal/AcceptPayment.js.map +1 -1
  39. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  40. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  41. package/dist/stripe/server/internal/html.gen.js +1 -1
  42. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  43. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  44. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  45. package/dist/tempo/server/internal/html.gen.js +1 -1
  46. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  47. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/request-body.js +2 -0
  49. package/dist/tempo/server/internal/request-body.js.map +1 -1
  50. package/dist/tempo/session/server/ChannelStore.js +4 -4
  51. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  52. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  53. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  54. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  55. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  56. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  57. package/dist/tempo/session/server/Session.js +1 -0
  58. package/dist/tempo/session/server/Session.js.map +1 -1
  59. package/dist/validation/core.d.ts +79 -0
  60. package/dist/validation/core.d.ts.map +1 -0
  61. package/dist/validation/core.js +256 -0
  62. package/dist/validation/core.js.map +1 -0
  63. package/dist/validation/index.d.ts +3 -0
  64. package/dist/validation/index.d.ts.map +1 -0
  65. package/dist/validation/index.js +2 -0
  66. package/dist/validation/index.js.map +1 -0
  67. package/dist/x402/Assets.d.ts +9 -0
  68. package/dist/x402/Assets.d.ts.map +1 -1
  69. package/dist/x402/Assets.js +38 -0
  70. package/dist/x402/Assets.js.map +1 -1
  71. package/package.json +9 -4
  72. package/src/cli/internal.ts +2 -2
  73. package/src/cli/plugins/evm.ts +55 -0
  74. package/src/cli/plugins/index.ts +1 -0
  75. package/src/cli/validate/challenge.ts +334 -121
  76. package/src/cli/validate/discovery.test.ts +307 -0
  77. package/src/cli/validate/discovery.ts +104 -38
  78. package/src/cli/validate/helpers.ts +15 -13
  79. package/src/cli/validate/index.ts +117 -240
  80. package/src/cli/validate/payment.ts +261 -178
  81. package/src/cli/validate.test.ts +143 -3
  82. package/src/client/internal/Fetch.test.ts +142 -2
  83. package/src/evm/Chains.ts +6 -0
  84. package/src/evm/PublicInterface.test-d.ts +7 -0
  85. package/src/internal/AcceptPayment.test.ts +45 -0
  86. package/src/internal/AcceptPayment.ts +11 -1
  87. package/src/stripe/server/internal/html/package.json +1 -1
  88. package/src/stripe/server/internal/html.gen.ts +1 -1
  89. package/src/tempo/server/internal/html/package.json +1 -1
  90. package/src/tempo/server/internal/html.gen.ts +1 -1
  91. package/src/tempo/server/internal/request-body.test.ts +9 -0
  92. package/src/tempo/server/internal/request-body.ts +2 -0
  93. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  94. package/src/tempo/session/server/ChannelStore.ts +4 -4
  95. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  96. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  97. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  98. package/src/tempo/session/server/Session.test.ts +119 -53
  99. package/src/tempo/session/server/Session.ts +1 -0
  100. package/src/validation/core.ts +348 -0
  101. package/src/validation/index.ts +11 -0
  102. package/src/x402/Assets.test.ts +42 -0
  103. package/src/x402/Assets.ts +40 -0
  104. package/src/x402/Exact.localnet.test.ts +475 -0
@@ -0,0 +1,348 @@
1
+ import { validateChallenge, validateErrorHandling } from '../cli/validate/challenge.js'
2
+ import {
3
+ buildUrl,
4
+ extractEndpointsFromDiscovery,
5
+ extractRequestBodyFromDiscovery,
6
+ fetchDiscoveryDoc,
7
+ } from '../cli/validate/discovery.js'
8
+ import type { CheckResult, EndpointSpec, PathParameter } from '../cli/validate/helpers.js'
9
+ import {
10
+ isValidIntegerAmount,
11
+ parseEndpointArg,
12
+ resolveBodyForEndpoint,
13
+ } from '../cli/validate/helpers.js'
14
+ import { validatePaymentFlow } from '../cli/validate/payment.js'
15
+ import { validate as validateDiscoveryDoc } from '../discovery/Validate.js'
16
+
17
+ export { buildUrl }
18
+ export type { CheckResult, EndpointSpec, PathParameter }
19
+
20
+ export type ValidateOptions = {
21
+ url: string
22
+ endpoint?: string | undefined
23
+ body?: string | undefined
24
+ query?: string[] | undefined
25
+ verbose?: boolean | undefined
26
+ yes?: boolean | undefined
27
+ skipPayment?: boolean | undefined
28
+ discoveryPath?: string | undefined
29
+ onPaymentResults?: (results: CheckResult[]) => void
30
+ }
31
+
32
+ export type DiscoveryResult = {
33
+ found: boolean
34
+ valid: boolean
35
+ endpoints: EndpointSpec[]
36
+ checks: CheckResult[]
37
+ doc: Record<string, unknown> | null
38
+ }
39
+
40
+ export type EndpointValidationResult = {
41
+ method: string
42
+ path: string
43
+ challenge: CheckResult[]
44
+ errorHandling: CheckResult[]
45
+ payment: CheckResult[]
46
+ }
47
+
48
+ export type ValidateResult = {
49
+ url: string
50
+ discovery: DiscoveryResult
51
+ endpoints: EndpointValidationResult[]
52
+ summary: { passed: number; failed: number; warnings: number; skipped: number }
53
+ flags: {
54
+ sawMppEndpoint: boolean
55
+ sawNonMppPaymentEndpoint: boolean
56
+ sawMalformedChallenge: boolean
57
+ sawTestnet: boolean
58
+ sawMainnet: boolean
59
+ paymentSucceeded: boolean
60
+ }
61
+ }
62
+
63
+ export type ValidateEvent =
64
+ | { phase: 'discovery'; discovery: DiscoveryResult; results: CheckResult[] }
65
+ | { phase: 'endpoint'; endpoint: EndpointSpec }
66
+ | {
67
+ phase: 'challenge'
68
+ endpoint: EndpointSpec
69
+ results: CheckResult[]
70
+ isMpp: boolean
71
+ isTestnet: boolean
72
+ isNonMppPayment: boolean
73
+ isMalformedChallenge: boolean
74
+ }
75
+ | { phase: 'errorHandling'; endpoint: EndpointSpec; results: CheckResult[] }
76
+ | {
77
+ phase: 'payment'
78
+ endpoint: EndpointSpec
79
+ results: CheckResult[]
80
+ succeeded: boolean
81
+ body?: string | undefined
82
+ }
83
+
84
+ /** Streams validation results as each phase completes. */
85
+ export async function* validateStream(options: ValidateOptions): AsyncGenerator<ValidateEvent> {
86
+ const baseUrl = options.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
87
+ const verbose = options.verbose ?? false
88
+
89
+ const discovery = await runDiscovery(baseUrl, options)
90
+ yield { phase: 'discovery', discovery, results: discovery.checks }
91
+
92
+ for (const endpoint of discovery.endpoints) {
93
+ yield { phase: 'endpoint', endpoint }
94
+
95
+ let body: string | undefined
96
+ if (options.endpoint) {
97
+ body = options.body
98
+ } else {
99
+ body = resolveBodyForEndpoint(options.body, endpoint.path)
100
+ if (!body && discovery.doc) {
101
+ body = extractRequestBodyFromDiscovery(discovery.doc, endpoint)
102
+ }
103
+ }
104
+
105
+ const { results: challengeResults, resolvedBody } = await validateChallenge(
106
+ baseUrl,
107
+ endpoint,
108
+ verbose,
109
+ {
110
+ body,
111
+ query: options.query,
112
+ discoveryDoc: discovery.doc ?? undefined,
113
+ },
114
+ )
115
+ const effectiveBody = resolvedBody ?? body
116
+ const isMpp = challengeResults.some(
117
+ (r) => r.severity === 'pass' && r.label === 'Challenge parseable',
118
+ )
119
+ const isTestnet = challengeResults.some(
120
+ (r) =>
121
+ r.severity === 'pass' &&
122
+ r.label.endsWith('Valid currency address') &&
123
+ r.detail === 'testnet',
124
+ )
125
+ const isNonMppPayment =
126
+ !isMpp && challengeResults.some((r) => r.label === 'Not an MPP endpoint')
127
+ const hasPaymentScheme = challengeResults.some(
128
+ (r) => r.severity === 'pass' && r.label === 'WWW-Authenticate header present',
129
+ )
130
+ const challengeFailed = challengeResults.some(
131
+ (r) => r.severity === 'fail' && r.label === 'Challenge parseable',
132
+ )
133
+ const isMalformedChallenge = !isMpp && hasPaymentScheme && challengeFailed
134
+
135
+ yield {
136
+ phase: 'challenge',
137
+ endpoint,
138
+ results: challengeResults,
139
+ isMpp,
140
+ isTestnet,
141
+ isNonMppPayment,
142
+ isMalformedChallenge,
143
+ }
144
+
145
+ if (isMpp) {
146
+ const errorResults = await validateErrorHandling(baseUrl, endpoint, {
147
+ body: effectiveBody,
148
+ query: options.query,
149
+ })
150
+ yield { phase: 'errorHandling', endpoint, results: errorResults }
151
+
152
+ if (!options.skipPayment) {
153
+ const onResults = options.onPaymentResults
154
+ const paymentResults = await validatePaymentFlow(baseUrl, endpoint, verbose, {
155
+ body: effectiveBody,
156
+ query: options.query,
157
+ yes: options.yes,
158
+ ...(onResults && { onResults }),
159
+ })
160
+ const succeeded = paymentResults.some(
161
+ (r) => r.severity === 'pass' && r.label === 'Payment: successful',
162
+ )
163
+ yield { phase: 'payment', endpoint, results: paymentResults, succeeded }
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ /** Runs the full validation suite and returns all results as a batch. */
170
+ export async function validate(options: ValidateOptions): Promise<ValidateResult> {
171
+ const baseUrl = options.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
172
+ let discovery: DiscoveryResult | undefined
173
+ const endpointResults: EndpointValidationResult[] = []
174
+ let current: EndpointValidationResult | undefined
175
+
176
+ let sawTestnet = false
177
+ let sawMainnet = false
178
+ let paymentSucceeded = false
179
+ let sawMppEndpoint = false
180
+ let sawNonMppPaymentEndpoint = false
181
+ let sawMalformedChallenge = false
182
+
183
+ for await (const event of validateStream(options)) {
184
+ switch (event.phase) {
185
+ case 'discovery':
186
+ discovery = event.discovery
187
+ break
188
+ case 'endpoint':
189
+ current = {
190
+ method: event.endpoint.method,
191
+ path: event.endpoint.path,
192
+ challenge: [],
193
+ errorHandling: [],
194
+ payment: [],
195
+ }
196
+ endpointResults.push(current)
197
+ break
198
+ case 'challenge':
199
+ if (current) current.challenge = event.results
200
+ if (event.isMpp) {
201
+ sawMppEndpoint = true
202
+ if (event.isTestnet) sawTestnet = true
203
+ else sawMainnet = true
204
+ }
205
+ if (event.isNonMppPayment) sawNonMppPaymentEndpoint = true
206
+ if (event.isMalformedChallenge) sawMalformedChallenge = true
207
+ break
208
+ case 'errorHandling':
209
+ if (current) current.errorHandling = event.results
210
+ break
211
+ case 'payment':
212
+ if (current) current.payment = event.results
213
+ if (event.succeeded) paymentSucceeded = true
214
+ break
215
+ }
216
+ }
217
+
218
+ const summary = { passed: 0, failed: 0, warnings: 0, skipped: 0 }
219
+ if (discovery) {
220
+ for (const r of discovery.checks) {
221
+ if (r.severity === 'pass') summary.passed++
222
+ else if (r.severity === 'fail') summary.failed++
223
+ else if (r.severity === 'warn') summary.warnings++
224
+ else if (r.severity === 'skip') summary.skipped++
225
+ }
226
+ }
227
+ for (const ep of endpointResults) {
228
+ for (const r of [...ep.challenge, ...ep.errorHandling, ...ep.payment]) {
229
+ if (r.severity === 'pass') summary.passed++
230
+ else if (r.severity === 'fail') summary.failed++
231
+ else if (r.severity === 'warn') summary.warnings++
232
+ else if (r.severity === 'skip') summary.skipped++
233
+ }
234
+ }
235
+
236
+ if (!discovery) throw new Error('Discovery phase did not complete')
237
+
238
+ return {
239
+ url: baseUrl,
240
+ discovery,
241
+ endpoints: endpointResults,
242
+ summary,
243
+ flags: {
244
+ sawMppEndpoint,
245
+ sawNonMppPaymentEndpoint,
246
+ sawMalformedChallenge,
247
+ sawTestnet,
248
+ sawMainnet,
249
+ paymentSucceeded,
250
+ },
251
+ }
252
+ }
253
+
254
+ async function runDiscovery(baseUrl: string, options: ValidateOptions): Promise<DiscoveryResult> {
255
+ const checks: CheckResult[] = []
256
+ let endpoints: EndpointSpec[] = []
257
+ let discoveryDoc: Record<string, unknown> | null = null
258
+ let found = false
259
+ let valid = false
260
+
261
+ // Try the user's path first, then root, then /api.
262
+ const origin = new URL(baseUrl).origin
263
+ let candidates: string[]
264
+ if (options.discoveryPath) {
265
+ candidates = [
266
+ new URL(options.discoveryPath, baseUrl + '/').href.replace(/\/openapi\.json$/i, ''),
267
+ ]
268
+ } else {
269
+ candidates = [baseUrl, origin, `${origin}/api`]
270
+ candidates = [...new Set(candidates)]
271
+ }
272
+ let discoveryResult: Awaited<ReturnType<typeof fetchDiscoveryDoc>> | undefined
273
+ const attemptedErrors: string[] = []
274
+ for (const candidate of candidates) {
275
+ discoveryResult = await fetchDiscoveryDoc(candidate)
276
+ if ('error' in discoveryResult) {
277
+ attemptedErrors.push(`${candidate}/openapi.json: ${discoveryResult.error}`)
278
+ } else {
279
+ break
280
+ }
281
+ }
282
+
283
+ if (!discoveryResult || 'error' in discoveryResult) {
284
+ checks.push({
285
+ label: 'Document found',
286
+ detail: attemptedErrors.join('; '),
287
+ hint: 'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
288
+ severity: 'fail',
289
+ })
290
+ } else {
291
+ found = true
292
+ checks.push({ label: 'Document found and parseable', severity: 'pass' })
293
+
294
+ const issues = validateDiscoveryDoc(discoveryResult.doc)
295
+ const errors = issues.filter((i) => i.severity === 'error')
296
+ const warnings = issues.filter((i) => i.severity === 'warning')
297
+
298
+ if (errors.length > 0) {
299
+ checks.push({
300
+ label: 'Valid OpenAPI structure',
301
+ detail: `${errors.length} error(s)`,
302
+ severity: 'fail',
303
+ })
304
+ for (const e of errors) {
305
+ checks.push({ label: e.message, detail: e.path, severity: 'fail' })
306
+ }
307
+ } else {
308
+ valid = true
309
+ checks.push({ label: 'Valid OpenAPI structure', severity: 'pass' })
310
+ }
311
+
312
+ for (const w of warnings) {
313
+ checks.push({ label: w.message, detail: w.path, severity: 'warn' })
314
+ }
315
+
316
+ discoveryDoc = discoveryResult.doc as Record<string, unknown>
317
+ }
318
+
319
+ if (options.endpoint) {
320
+ const parsed = parseEndpointArg(options.endpoint)
321
+ if (parsed) endpoints.push(parsed)
322
+ } else if (discoveryDoc) {
323
+ endpoints = extractEndpointsFromDiscovery(discoveryDoc)
324
+
325
+ const NO_AMOUNT = BigInt('999999999999999999')
326
+ endpoints.sort((a, b) => {
327
+ const aAmt = isValidIntegerAmount(a.amount) ? BigInt(a.amount!) : NO_AMOUNT
328
+ const bAmt = isValidIntegerAmount(b.amount) ? BigInt(b.amount!) : NO_AMOUNT
329
+ return aAmt < bAmt ? -1 : aAmt > bAmt ? 1 : 0
330
+ })
331
+
332
+ if (endpoints.length > 0) {
333
+ checks.push({
334
+ label: 'Paid endpoints found',
335
+ detail: `${endpoints.length} endpoint(s)`,
336
+ severity: 'pass',
337
+ })
338
+ } else {
339
+ checks.push({
340
+ label: 'Paid endpoints found',
341
+ detail: 'No endpoints with x-payment-info',
342
+ severity: 'warn',
343
+ })
344
+ }
345
+ }
346
+
347
+ return { found, valid, endpoints, checks, doc: discoveryDoc }
348
+ }
@@ -0,0 +1,11 @@
1
+ export { buildUrl, validate, validateStream } from './core.js'
2
+ export type {
3
+ CheckResult,
4
+ DiscoveryResult,
5
+ EndpointSpec,
6
+ EndpointValidationResult,
7
+ PathParameter,
8
+ ValidateEvent,
9
+ ValidateOptions,
10
+ ValidateResult,
11
+ } from './core.js'
@@ -208,4 +208,46 @@ describe('x402 assets', () => {
208
208
  },
209
209
  })
210
210
  })
211
+
212
+ test('exports Celo USDC metadata', () => {
213
+ expect(Assets.isAsset(Assets.celo.USDC)).toBe(true)
214
+ expect(Assets.celo.USDC).toMatchObject({
215
+ address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
216
+ decimals: 6,
217
+ network: 'eip155:42220',
218
+ transfer: {
219
+ name: 'USDC',
220
+ type: 'eip3009',
221
+ version: '2',
222
+ },
223
+ })
224
+ })
225
+
226
+ test('exports Celo USDT metadata', () => {
227
+ expect(Assets.isAsset(Assets.celo.USDT)).toBe(true)
228
+ expect(Assets.celo.USDT).toMatchObject({
229
+ address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e',
230
+ decimals: 6,
231
+ network: 'eip155:42220',
232
+ transfer: {
233
+ name: 'Tether USD',
234
+ type: 'eip3009',
235
+ version: '1',
236
+ },
237
+ })
238
+ })
239
+
240
+ test('exports Celo Sepolia USDC metadata', () => {
241
+ expect(Assets.isAsset(Assets.celoSepolia.USDC)).toBe(true)
242
+ expect(Assets.celoSepolia.USDC).toMatchObject({
243
+ address: '0x01C5C0122039549AD1493B8220cABEdD739BC44E',
244
+ decimals: 6,
245
+ network: 'eip155:11142220',
246
+ transfer: {
247
+ name: 'USDC',
248
+ type: 'eip3009',
249
+ version: '2',
250
+ },
251
+ })
252
+ })
211
253
  })
@@ -91,6 +91,46 @@ export const baseSepolia = {
91
91
  }),
92
92
  } as const
93
93
 
94
+ /** Celo network known assets. */
95
+ export const celo = {
96
+ USDC: define({
97
+ address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
98
+ decimals: 6,
99
+ network: 'eip155:42220',
100
+ transfer: {
101
+ // Celo USDC signs with the shorter EIP-712 domain name.
102
+ name: 'USDC',
103
+ type: 'eip3009',
104
+ version: '2',
105
+ },
106
+ }),
107
+ USDT: define({
108
+ address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e',
109
+ decimals: 6,
110
+ network: 'eip155:42220',
111
+ transfer: {
112
+ // Celo USDT signs with domain version "1", unlike Circle's FiatToken "2".
113
+ name: 'Tether USD',
114
+ type: 'eip3009',
115
+ version: '1',
116
+ },
117
+ }),
118
+ } as const
119
+
120
+ /** Celo Sepolia known assets. */
121
+ export const celoSepolia = {
122
+ USDC: define({
123
+ address: '0x01C5C0122039549AD1493B8220cABEdD739BC44E',
124
+ decimals: 6,
125
+ network: 'eip155:11142220',
126
+ transfer: {
127
+ name: 'USDC',
128
+ type: 'eip3009',
129
+ version: '2',
130
+ },
131
+ }),
132
+ } as const
133
+
94
134
  /** Returns true when a value is known x402 asset metadata. */
95
135
  export function isAsset(value: unknown): value is KnownAsset {
96
136
  if (typeof value !== 'object' || value === null) return false