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,475 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import type { IncomingMessage } from 'node:http'
3
+ import { isDeepStrictEqual } from 'node:util'
4
+
5
+ import { evm as evmClient, Mppx as ClientMppx } from 'mppx/client'
6
+ import { evm, Mppx as ServerMppx, NodeListener, Request as ServerRequest } from 'mppx/server'
7
+ import type { Abi, Address, Hex } from 'viem'
8
+ import {
9
+ createClient,
10
+ defineChain,
11
+ getAddress,
12
+ http as viem_http,
13
+ parseSignature,
14
+ parseUnits,
15
+ recoverTypedDataAddress,
16
+ } from 'viem'
17
+ import { mnemonicToAccount } from 'viem/accounts'
18
+ import {
19
+ deployContract,
20
+ readContract,
21
+ waitForTransactionReceipt,
22
+ writeContract,
23
+ } from 'viem/actions'
24
+ import { describe, expect, test } from 'vp/test'
25
+ import * as Http from '~test/Http.js'
26
+
27
+ import * as evm_Types from '../evm/Types.js'
28
+ import * as Header from './Header.js'
29
+ import * as x402_ChallengeBrand from './internal/ChallengeBrand.js'
30
+ import * as Types from './Types.js'
31
+
32
+ const runLocalnet = process.env.X402_LOCALNET === 'true'
33
+ const describeLocalnet = runLocalnet ? describe : describe.skip
34
+
35
+ const chainId = 31_337
36
+ const rpcUrl = process.env.X402_ANVIL_RPC_URL ?? 'http://127.0.0.1:18546'
37
+ const mnemonic = 'test test test test test test test test test test test junk'
38
+ const payer = mnemonicToAccount(mnemonic, { accountIndex: 0 })
39
+ const recipient = mnemonicToAccount(mnemonic, { addressIndex: 1 })
40
+ const facilitatorAccount = mnemonicToAccount(mnemonic, { addressIndex: 2 })
41
+ const paymentAmount = parseUnits('0.01', 6)
42
+
43
+ const chain = defineChain({
44
+ id: chainId,
45
+ name: 'Anvil',
46
+ nativeCurrency: { decimals: 18, name: 'Ether', symbol: 'ETH' },
47
+ rpcUrls: { default: { http: [rpcUrl] } },
48
+ })
49
+
50
+ const transport = viem_http(rpcUrl, { retryCount: 0, timeout: 10_000 })
51
+ const payerClient = createClient({ account: payer, chain, transport })
52
+ const facilitatorClient = createClient({ account: facilitatorAccount, chain, transport })
53
+
54
+ type ForgeArtifact = {
55
+ abi: Abi
56
+ bytecode: { object: Hex }
57
+ }
58
+
59
+ type Harness = {
60
+ artifact: ForgeArtifact
61
+ asset: ReturnType<typeof evm.assets.define>
62
+ facilitator: Http.TestServer
63
+ token: Address
64
+ }
65
+
66
+ describeLocalnet('x402 exact localnet settlement', () => {
67
+ test('settles x402 exact EIP-3009 payment on localnet', async () => {
68
+ const harness = await setupHarness()
69
+ const paidServer = await createPaidServer(harness)
70
+
71
+ try {
72
+ const challenge = await fetch(`${paidServer.url}/paid`)
73
+ expect(challenge.status).toBe(402)
74
+ const paymentRequiredHeader = challenge.headers.get(Types.paymentRequiredHeader)
75
+ expect(paymentRequiredHeader).toBeTruthy()
76
+
77
+ const paymentRequired = Header.decodePaymentRequired(paymentRequiredHeader!)
78
+ expect(paymentRequired.x402Version).toBe(2)
79
+ expect(paymentRequired.accepts[0]).toMatchObject({
80
+ amount: paymentAmount.toString(),
81
+ asset: getAddress(harness.token),
82
+ network: `eip155:${chainId}`,
83
+ payTo: recipient.address,
84
+ scheme: 'exact',
85
+ })
86
+
87
+ const payerBefore = await balanceOf(harness, payer.address)
88
+ const recipientBefore = await balanceOf(harness, recipient.address)
89
+ const payment = createX402Client(harness.asset)
90
+ const response = await payment.fetch(`${paidServer.url}/paid`)
91
+
92
+ if (response.status !== 200)
93
+ throw new Error(`Expected paid response, got ${response.status}: ${await response.text()}`)
94
+ expect(response.status).toBe(200)
95
+ expect(await response.text()).toBe('paid')
96
+
97
+ const paymentResponseHeader = response.headers.get(Types.paymentResponseHeader)
98
+ expect(paymentResponseHeader).toBeTruthy()
99
+ const paymentResponse = Header.decodePaymentResponse(paymentResponseHeader!)
100
+ expect(paymentResponse).toMatchObject({
101
+ network: `eip155:${chainId}`,
102
+ payer: getAddress(payer.address),
103
+ success: true,
104
+ })
105
+
106
+ const receipt = await waitForTransactionReceipt(payerClient, {
107
+ hash: paymentResponse.transaction as Hex,
108
+ })
109
+ expect(receipt.status).toBe('success')
110
+ expect(await balanceOf(harness, payer.address)).toBe(payerBefore - paymentAmount)
111
+ expect(await balanceOf(harness, recipient.address)).toBe(recipientBefore + paymentAmount)
112
+ } finally {
113
+ paidServer.close()
114
+ harness.facilitator.close()
115
+ }
116
+ })
117
+
118
+ test('rejects replayed localnet x402 payment', async () => {
119
+ const harness = await setupHarness()
120
+ const paidServer = await createPaidServer(harness)
121
+
122
+ try {
123
+ const payment = createX402Client(harness.asset)
124
+ const challenge = await payment.rawFetch(`${paidServer.url}/paid`)
125
+ expect(challenge.status).toBe(402)
126
+
127
+ const credential = await payment.createCredential(pureX402Challenge(challenge))
128
+ const recipientBefore = await balanceOf(harness, recipient.address)
129
+
130
+ const first = await payment.rawFetch(`${paidServer.url}/paid`, {
131
+ headers: { [Types.paymentSignatureHeader]: credential },
132
+ })
133
+ expect(first.status).toBe(200)
134
+
135
+ const second = await payment.rawFetch(`${paidServer.url}/paid`, {
136
+ headers: { [Types.paymentSignatureHeader]: credential },
137
+ })
138
+ expect(second.status).toBe(402)
139
+ expect(await balanceOf(harness, recipient.address)).toBe(recipientBefore + paymentAmount)
140
+ expect(facilitatorStats(harness.facilitator).settleRequests).toBe(1)
141
+ } finally {
142
+ paidServer.close()
143
+ harness.facilitator.close()
144
+ }
145
+ })
146
+ })
147
+
148
+ async function setupHarness(): Promise<Harness> {
149
+ const artifact = await loadArtifact()
150
+ const token = await deployToken(artifact)
151
+ const asset = evm.assets.define({
152
+ address: token,
153
+ decimals: 6,
154
+ network: `eip155:${chainId}`,
155
+ transfer: {
156
+ name: 'USDC',
157
+ type: 'eip3009',
158
+ version: '2',
159
+ },
160
+ })
161
+
162
+ await mint(artifact, token, payer.address, parseUnits('1000', 6))
163
+
164
+ return {
165
+ artifact,
166
+ asset,
167
+ facilitator: await createFacilitator({ artifact, token }),
168
+ token,
169
+ }
170
+ }
171
+
172
+ async function loadArtifact(): Promise<ForgeArtifact> {
173
+ try {
174
+ const path = new URL('../../_/foundry/out/TestUSDC.sol/TestUSDC.json', import.meta.url)
175
+ const artifact = JSON.parse(await readFile(path, 'utf8')) as ForgeArtifact
176
+ if (!artifact.bytecode.object) throw new Error()
177
+ return artifact
178
+ } catch {
179
+ throw new Error('Missing TestUSDC Forge artifact. Run `forge build` before localnet tests.')
180
+ }
181
+ }
182
+
183
+ async function deployToken(artifact: ForgeArtifact): Promise<Address> {
184
+ const hash = await deployContract(payerClient, {
185
+ abi: artifact.abi,
186
+ bytecode: artifact.bytecode.object,
187
+ })
188
+ const receipt = await waitForTransactionReceipt(payerClient, { hash })
189
+ if (!receipt.contractAddress) throw new Error('TestUSDC deploy did not return a contract.')
190
+ return receipt.contractAddress
191
+ }
192
+
193
+ async function mint(
194
+ artifact: ForgeArtifact,
195
+ token: Address,
196
+ to: Address,
197
+ amount: bigint,
198
+ ): Promise<void> {
199
+ const hash = await writeContract(payerClient, {
200
+ abi: artifact.abi,
201
+ address: token,
202
+ functionName: 'mint',
203
+ args: [to, amount],
204
+ })
205
+ await waitForTransactionReceipt(payerClient, { hash })
206
+ }
207
+
208
+ async function balanceOf(harness: Harness, address: Address): Promise<bigint> {
209
+ return readContract(payerClient, {
210
+ abi: harness.artifact.abi,
211
+ address: harness.token,
212
+ functionName: 'balanceOf',
213
+ args: [address],
214
+ }) as Promise<bigint>
215
+ }
216
+
217
+ async function authorizationState(parameters: {
218
+ artifact: ForgeArtifact
219
+ from: Address
220
+ nonce: Hex
221
+ token: Address
222
+ }): Promise<boolean> {
223
+ return readContract(payerClient, {
224
+ abi: parameters.artifact.abi,
225
+ address: parameters.token,
226
+ functionName: 'authorizationState',
227
+ args: [parameters.from, parameters.nonce],
228
+ }) as Promise<boolean>
229
+ }
230
+
231
+ async function createPaidServer(harness: Harness): Promise<Http.TestServer> {
232
+ const payment = ServerMppx.create({
233
+ methods: [
234
+ evm.charge({
235
+ currency: harness.asset,
236
+ recipient: recipient.address,
237
+ x402: { facilitator: harness.facilitator.url },
238
+ }),
239
+ ],
240
+ secretKey: 'x402-localnet-secret-key-32-bytes',
241
+ })
242
+ const paid = payment.evm.charge({ amount: '0.01', description: 'localnet x402' })
243
+
244
+ return Http.createServer(async (req, res) => {
245
+ if (req.url !== '/paid') {
246
+ return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
247
+ }
248
+
249
+ const result = await paid(ServerRequest.fromNodeListener(req, res))
250
+ if (result.status === 402) return NodeListener.sendResponse(res, result.challenge)
251
+ return NodeListener.sendResponse(res, result.withReceipt(new Response('paid')))
252
+ })
253
+ }
254
+
255
+ function createX402Client(asset: ReturnType<typeof evm.assets.define>) {
256
+ return ClientMppx.create({
257
+ methods: [
258
+ evmClient.charge({
259
+ account: payer,
260
+ currencies: [asset],
261
+ maxAmount: '0.02',
262
+ networks: [chainId],
263
+ }),
264
+ ],
265
+ orderChallenges: (candidates) =>
266
+ [...candidates].sort(
267
+ (a, b) =>
268
+ Number(x402_ChallengeBrand.is(b.challenge)) - Number(x402_ChallengeBrand.is(a.challenge)),
269
+ ),
270
+ polyfill: false,
271
+ })
272
+ }
273
+
274
+ async function createFacilitator(parameters: {
275
+ artifact: ForgeArtifact
276
+ token: Address
277
+ }): Promise<Http.TestServer> {
278
+ const stats = { settleRequests: 0, verifyRequests: 0 }
279
+ const server = await Http.createServer(async (req, res) => {
280
+ if (req.method !== 'POST') {
281
+ return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
282
+ }
283
+
284
+ const body = await readJson(req)
285
+ if (req.url === '/verify') {
286
+ stats.verifyRequests++
287
+ const verified = await verifyPayment({ ...parameters, body })
288
+ return NodeListener.sendResponse(res, Response.json(verified))
289
+ }
290
+
291
+ if (req.url === '/settle') {
292
+ stats.settleRequests++
293
+ const settled = await settlePayment({ ...parameters, body })
294
+ return NodeListener.sendResponse(res, Response.json(settled))
295
+ }
296
+
297
+ return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
298
+ })
299
+
300
+ return Object.assign(server, { x402LocalnetStats: stats })
301
+ }
302
+
303
+ async function readJson(req: IncomingMessage) {
304
+ const chunks: Buffer[] = []
305
+ for await (const chunk of req) chunks.push(Buffer.from(chunk))
306
+ return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
307
+ }
308
+
309
+ async function verifyPayment(parameters: {
310
+ artifact: ForgeArtifact
311
+ body: unknown
312
+ token: Address
313
+ }): Promise<Types.VerifyResponse> {
314
+ try {
315
+ const { paymentPayload, paymentRequirements, x402Version } = parseFacilitatorRequest(
316
+ parameters.body,
317
+ )
318
+ if (x402Version !== 2) return invalid('unsupported x402 version')
319
+ if (!isDeepStrictEqual(paymentPayload.accepted, paymentRequirements))
320
+ return invalid('payment payload accepted requirements mismatch')
321
+ if (!('authorization' in paymentPayload.payload))
322
+ return invalid('expected EIP-3009 authorization payload')
323
+
324
+ const authorization = paymentPayload.payload.authorization
325
+ if (getAddress(paymentRequirements.payTo as Address) !== getAddress(authorization.to))
326
+ return invalid('authorization recipient mismatch')
327
+ if (paymentRequirements.amount !== authorization.value)
328
+ return invalid('authorization amount mismatch')
329
+
330
+ const now = BigInt(Math.floor(Date.now() / 1000))
331
+ if (BigInt(authorization.validAfter) > now) return invalid('authorization is not valid yet')
332
+ if (BigInt(authorization.validBefore) <= now) return invalid('authorization has expired')
333
+
334
+ const recovered = await recoverTypedDataAddress({
335
+ domain: {
336
+ chainId,
337
+ name: stringExtra(paymentRequirements, 'name'),
338
+ verifyingContract: getAddress(paymentRequirements.asset as Address),
339
+ version: stringExtra(paymentRequirements, 'version'),
340
+ },
341
+ message: {
342
+ from: getAddress(authorization.from),
343
+ nonce: authorization.nonce as Hex,
344
+ to: getAddress(authorization.to),
345
+ validAfter: BigInt(authorization.validAfter),
346
+ validBefore: BigInt(authorization.validBefore),
347
+ value: BigInt(authorization.value),
348
+ },
349
+ primaryType: 'TransferWithAuthorization',
350
+ signature: paymentPayload.payload.signature as Hex,
351
+ types: evm_Types.authorizationTypes,
352
+ })
353
+ if (getAddress(recovered) !== getAddress(authorization.from))
354
+ return invalid('authorization signature mismatch')
355
+
356
+ const used = await authorizationState({
357
+ artifact: parameters.artifact,
358
+ from: authorization.from as Address,
359
+ nonce: authorization.nonce as Hex,
360
+ token: parameters.token,
361
+ })
362
+ if (used) return invalid('authorization nonce already used')
363
+
364
+ return { isValid: true, payer: getAddress(authorization.from) }
365
+ } catch (error) {
366
+ return invalid(error instanceof Error ? error.message : 'invalid payment')
367
+ }
368
+ }
369
+
370
+ async function settlePayment(parameters: {
371
+ artifact: ForgeArtifact
372
+ body: unknown
373
+ token: Address
374
+ }): Promise<Types.SettleResponse> {
375
+ const verified = await verifyPayment(parameters)
376
+ const { paymentPayload, paymentRequirements } = parseFacilitatorRequest(parameters.body)
377
+ const network = paymentRequirements.network
378
+
379
+ if (!verified.isValid) {
380
+ return {
381
+ errorReason: verified.invalidReason ?? verified.invalidMessage ?? 'verification failed',
382
+ network,
383
+ success: false,
384
+ transaction: '',
385
+ }
386
+ }
387
+ if (!('authorization' in paymentPayload.payload)) {
388
+ return {
389
+ errorReason: 'expected EIP-3009 authorization payload',
390
+ network,
391
+ success: false,
392
+ transaction: '',
393
+ }
394
+ }
395
+
396
+ try {
397
+ const { authorization, signature } = paymentPayload.payload
398
+ const { r, s, v, yParity } = parseSignature(signature as Hex)
399
+ const hash = await writeContract(facilitatorClient, {
400
+ abi: parameters.artifact.abi,
401
+ address: parameters.token,
402
+ functionName: 'transferWithAuthorization',
403
+ args: [
404
+ authorization.from,
405
+ authorization.to,
406
+ BigInt(authorization.value),
407
+ BigInt(authorization.validAfter),
408
+ BigInt(authorization.validBefore),
409
+ authorization.nonce,
410
+ Number(v ?? (yParity === 0 ? 27 : 28)),
411
+ r,
412
+ s,
413
+ ],
414
+ })
415
+ await waitForTransactionReceipt(facilitatorClient, { hash })
416
+ return {
417
+ amount: authorization.value,
418
+ network,
419
+ payer: getAddress(authorization.from),
420
+ success: true,
421
+ transaction: hash,
422
+ }
423
+ } catch (error) {
424
+ return {
425
+ errorReason: error instanceof Error ? error.message : 'settlement failed',
426
+ network,
427
+ success: false,
428
+ transaction: '',
429
+ }
430
+ }
431
+ }
432
+
433
+ function parseFacilitatorRequest(body: unknown): {
434
+ paymentPayload: Types.PaymentPayload
435
+ paymentRequirements: Types.PaymentRequirements
436
+ x402Version: unknown
437
+ } {
438
+ if (!body || typeof body !== 'object') throw new Error('invalid facilitator request')
439
+ const record = body as Record<string, unknown>
440
+ return {
441
+ paymentPayload: Types.PaymentPayloadSchema.parse(record.paymentPayload),
442
+ paymentRequirements: Types.PaymentRequirementsSchema.parse(record.paymentRequirements),
443
+ x402Version: record.x402Version,
444
+ }
445
+ }
446
+
447
+ function stringExtra(paymentRequirements: Types.PaymentRequirements, key: string): string {
448
+ const value = paymentRequirements.extra?.[key]
449
+ if (typeof value !== 'string') throw new Error(`missing ${key} metadata`)
450
+ return value
451
+ }
452
+
453
+ function invalid(reason: string): Types.VerifyResponse {
454
+ return { invalidReason: reason, isValid: false }
455
+ }
456
+
457
+ function pureX402Challenge(response: Response): Response {
458
+ const paymentRequired = response.headers.get(Types.paymentRequiredHeader)
459
+ if (!paymentRequired) throw new Error('Missing PAYMENT-REQUIRED header.')
460
+ return new Response(null, {
461
+ headers: { [Types.paymentRequiredHeader]: paymentRequired },
462
+ status: 402,
463
+ })
464
+ }
465
+
466
+ function facilitatorStats(server: Http.TestServer): {
467
+ settleRequests: number
468
+ verifyRequests: number
469
+ } {
470
+ return (
471
+ server as Http.TestServer & {
472
+ x402LocalnetStats: { settleRequests: number; verifyRequests: number }
473
+ }
474
+ ).x402LocalnetStats
475
+ }