mppx 0.6.28 → 0.6.29

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 (272) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +16 -10
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Method.d.ts +1 -1
  6. package/dist/Method.d.ts.map +1 -1
  7. package/dist/client/Methods.d.ts +1 -0
  8. package/dist/client/Methods.d.ts.map +1 -1
  9. package/dist/client/Methods.js +1 -0
  10. package/dist/client/Methods.js.map +1 -1
  11. package/dist/client/Mppx.d.ts +3 -3
  12. package/dist/client/Mppx.d.ts.map +1 -1
  13. package/dist/client/Mppx.js +1 -0
  14. package/dist/client/Mppx.js.map +1 -1
  15. package/dist/client/Transport.d.ts +10 -3
  16. package/dist/client/Transport.d.ts.map +1 -1
  17. package/dist/client/Transport.js +60 -7
  18. package/dist/client/Transport.js.map +1 -1
  19. package/dist/client/index.d.ts +1 -1
  20. package/dist/client/index.d.ts.map +1 -1
  21. package/dist/client/index.js +1 -1
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/internal/Fetch.d.ts +3 -0
  24. package/dist/client/internal/Fetch.d.ts.map +1 -1
  25. package/dist/client/internal/Fetch.js +12 -20
  26. package/dist/client/internal/Fetch.js.map +1 -1
  27. package/dist/evm/Assets.d.ts +2 -0
  28. package/dist/evm/Assets.d.ts.map +1 -0
  29. package/dist/evm/Assets.js +2 -0
  30. package/dist/evm/Assets.js.map +1 -0
  31. package/dist/evm/Chains.d.ts +5 -0
  32. package/dist/evm/Chains.d.ts.map +1 -0
  33. package/dist/evm/Chains.js +5 -0
  34. package/dist/evm/Chains.js.map +1 -0
  35. package/dist/evm/Methods.d.ts +68 -0
  36. package/dist/evm/Methods.d.ts.map +1 -0
  37. package/dist/evm/Methods.js +28 -0
  38. package/dist/evm/Methods.js.map +1 -0
  39. package/dist/evm/Types.d.ts +143 -0
  40. package/dist/evm/Types.d.ts.map +1 -0
  41. package/dist/evm/Types.js +102 -0
  42. package/dist/evm/Types.js.map +1 -0
  43. package/dist/evm/client/Charge.d.ts +102 -0
  44. package/dist/evm/client/Charge.d.ts.map +1 -0
  45. package/dist/evm/client/Charge.js +141 -0
  46. package/dist/evm/client/Charge.js.map +1 -0
  47. package/dist/evm/client/Methods.d.ts +81 -0
  48. package/dist/evm/client/Methods.d.ts.map +1 -0
  49. package/dist/evm/client/Methods.js +16 -0
  50. package/dist/evm/client/Methods.js.map +1 -0
  51. package/dist/evm/client/index.d.ts +6 -0
  52. package/dist/evm/client/index.d.ts.map +1 -0
  53. package/dist/evm/client/index.js +6 -0
  54. package/dist/evm/client/index.js.map +1 -0
  55. package/dist/evm/index.d.ts +9 -0
  56. package/dist/evm/index.d.ts.map +1 -0
  57. package/dist/evm/index.js +8 -0
  58. package/dist/evm/index.js.map +1 -0
  59. package/dist/evm/server/Charge.d.ts +62 -0
  60. package/dist/evm/server/Charge.d.ts.map +1 -0
  61. package/dist/evm/server/Charge.js +172 -0
  62. package/dist/evm/server/Charge.js.map +1 -0
  63. package/dist/evm/server/Methods.d.ts +80 -0
  64. package/dist/evm/server/Methods.d.ts.map +1 -0
  65. package/dist/evm/server/Methods.js +16 -0
  66. package/dist/evm/server/Methods.js.map +1 -0
  67. package/dist/evm/server/index.d.ts +6 -0
  68. package/dist/evm/server/index.d.ts.map +1 -0
  69. package/dist/evm/server/index.js +6 -0
  70. package/dist/evm/server/index.js.map +1 -0
  71. package/dist/index.d.ts +2 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +2 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/internal/HeaderCodec.d.ts +18 -0
  76. package/dist/internal/HeaderCodec.d.ts.map +1 -0
  77. package/dist/internal/HeaderCodec.js +31 -0
  78. package/dist/internal/HeaderCodec.js.map +1 -0
  79. package/dist/middlewares/elysia.d.ts.map +1 -1
  80. package/dist/middlewares/elysia.js +2 -3
  81. package/dist/middlewares/elysia.js.map +1 -1
  82. package/dist/middlewares/express.js +2 -1
  83. package/dist/middlewares/express.js.map +1 -1
  84. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  85. package/dist/proxy/internal/Headers.js +11 -1
  86. package/dist/proxy/internal/Headers.js.map +1 -1
  87. package/dist/proxy/services/openai.d.ts.map +1 -1
  88. package/dist/proxy/services/openai.js +2 -0
  89. package/dist/proxy/services/openai.js.map +1 -1
  90. package/dist/server/Methods.d.ts +1 -0
  91. package/dist/server/Methods.d.ts.map +1 -1
  92. package/dist/server/Methods.js +1 -0
  93. package/dist/server/Methods.js.map +1 -1
  94. package/dist/server/Mppx.d.ts.map +1 -1
  95. package/dist/server/Mppx.js +90 -12
  96. package/dist/server/Mppx.js.map +1 -1
  97. package/dist/server/Transport.d.ts +10 -0
  98. package/dist/server/Transport.d.ts.map +1 -1
  99. package/dist/server/Transport.js +9 -0
  100. package/dist/server/Transport.js.map +1 -1
  101. package/dist/server/index.d.ts +1 -1
  102. package/dist/server/index.d.ts.map +1 -1
  103. package/dist/server/index.js +1 -1
  104. package/dist/server/index.js.map +1 -1
  105. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  106. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  107. package/dist/stripe/server/internal/html.gen.js +1 -1
  108. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  109. package/dist/tempo/client/ChannelOps.d.ts +3 -3
  110. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  111. package/dist/tempo/client/ChannelOps.js +13 -6
  112. package/dist/tempo/client/ChannelOps.js.map +1 -1
  113. package/dist/tempo/client/Charge.d.ts.map +1 -1
  114. package/dist/tempo/client/Charge.js +8 -5
  115. package/dist/tempo/client/Charge.js.map +1 -1
  116. package/dist/tempo/client/Methods.d.ts +0 -1
  117. package/dist/tempo/client/Methods.d.ts.map +1 -1
  118. package/dist/tempo/client/Session.d.ts +2 -4
  119. package/dist/tempo/client/Session.d.ts.map +1 -1
  120. package/dist/tempo/client/Session.js +10 -12
  121. package/dist/tempo/client/Session.js.map +1 -1
  122. package/dist/tempo/client/SessionManager.d.ts +3 -3
  123. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  124. package/dist/tempo/client/SessionManager.js +1 -1
  125. package/dist/tempo/client/SessionManager.js.map +1 -1
  126. package/dist/tempo/internal/account.d.ts +5 -0
  127. package/dist/tempo/internal/account.d.ts.map +1 -1
  128. package/dist/tempo/internal/account.js +8 -0
  129. package/dist/tempo/internal/account.js.map +1 -1
  130. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  131. package/dist/tempo/internal/fee-payer.js +5 -2
  132. package/dist/tempo/internal/fee-payer.js.map +1 -1
  133. package/dist/tempo/server/Charge.d.ts.map +1 -1
  134. package/dist/tempo/server/Charge.js +23 -1
  135. package/dist/tempo/server/Charge.js.map +1 -1
  136. package/dist/tempo/server/Session.d.ts.map +1 -1
  137. package/dist/tempo/server/Session.js +13 -12
  138. package/dist/tempo/server/Session.js.map +1 -1
  139. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  140. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  141. package/dist/tempo/server/internal/html.gen.js +1 -1
  142. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  143. package/dist/tempo/session/Chain.d.ts +2 -0
  144. package/dist/tempo/session/Chain.d.ts.map +1 -1
  145. package/dist/tempo/session/Chain.js +8 -8
  146. package/dist/tempo/session/Chain.js.map +1 -1
  147. package/dist/tempo/session/Voucher.d.ts +4 -3
  148. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  149. package/dist/tempo/session/Voucher.js +71 -44
  150. package/dist/tempo/session/Voucher.js.map +1 -1
  151. package/dist/tempo/session/Ws.d.ts.map +1 -1
  152. package/dist/tempo/session/Ws.js +15 -0
  153. package/dist/tempo/session/Ws.js.map +1 -1
  154. package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
  155. package/dist/x402/Assets.d.ts +29 -0
  156. package/dist/x402/Assets.d.ts.map +1 -0
  157. package/dist/x402/Assets.js +46 -0
  158. package/dist/x402/Assets.js.map +1 -0
  159. package/dist/x402/Header.d.ts +14 -0
  160. package/dist/x402/Header.d.ts.map +1 -0
  161. package/dist/x402/Header.js +18 -0
  162. package/dist/x402/Header.js.map +1 -0
  163. package/dist/x402/Types.d.ts +289 -0
  164. package/dist/x402/Types.d.ts.map +1 -0
  165. package/dist/x402/Types.js +139 -0
  166. package/dist/x402/Types.js.map +1 -0
  167. package/dist/x402/client/Exact.d.ts +38 -0
  168. package/dist/x402/client/Exact.d.ts.map +1 -0
  169. package/dist/x402/client/Exact.js +141 -0
  170. package/dist/x402/client/Exact.js.map +1 -0
  171. package/dist/x402/index.d.ts +6 -0
  172. package/dist/x402/index.d.ts.map +1 -0
  173. package/dist/x402/index.js +6 -0
  174. package/dist/x402/index.js.map +1 -0
  175. package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
  176. package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
  177. package/dist/x402/internal/ChallengeBrand.js +13 -0
  178. package/dist/x402/internal/ChallengeBrand.js.map +1 -0
  179. package/dist/x402/internal/RouteBinding.d.ts +8 -0
  180. package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
  181. package/dist/x402/internal/RouteBinding.js +12 -0
  182. package/dist/x402/internal/RouteBinding.js.map +1 -0
  183. package/dist/x402/server/EvmCharge.d.ts +50 -0
  184. package/dist/x402/server/EvmCharge.d.ts.map +1 -0
  185. package/dist/x402/server/EvmCharge.js +301 -0
  186. package/dist/x402/server/EvmCharge.js.map +1 -0
  187. package/dist/x402/server/Facilitator.d.ts +12 -0
  188. package/dist/x402/server/Facilitator.d.ts.map +1 -0
  189. package/dist/x402/server/Facilitator.js +42 -0
  190. package/dist/x402/server/Facilitator.js.map +1 -0
  191. package/package.json +41 -21
  192. package/src/Challenge.test.ts +28 -0
  193. package/src/Challenge.ts +17 -10
  194. package/src/Method.ts +1 -1
  195. package/src/client/Methods.ts +1 -0
  196. package/src/client/Mppx.ts +4 -3
  197. package/src/client/Transport.test.ts +165 -30
  198. package/src/client/Transport.ts +76 -8
  199. package/src/client/index.ts +1 -1
  200. package/src/client/internal/Fetch.test.ts +31 -2
  201. package/src/client/internal/Fetch.ts +26 -19
  202. package/src/evm/Assets.ts +1 -0
  203. package/src/evm/Chains.ts +5 -0
  204. package/src/evm/Methods.ts +44 -0
  205. package/src/evm/PublicInterface.test-d.ts +109 -0
  206. package/src/evm/Types.ts +140 -0
  207. package/src/evm/client/Charge.test.ts +99 -0
  208. package/src/evm/client/Charge.ts +198 -0
  209. package/src/evm/client/Methods.ts +19 -0
  210. package/src/evm/client/index.ts +5 -0
  211. package/src/evm/index.ts +13 -0
  212. package/src/evm/server/Charge.test.ts +199 -0
  213. package/src/evm/server/Charge.ts +283 -0
  214. package/src/evm/server/Methods.ts +22 -0
  215. package/src/evm/server/index.ts +5 -0
  216. package/src/index.ts +2 -0
  217. package/src/internal/HeaderCodec.ts +36 -0
  218. package/src/middlewares/elysia.test.ts +25 -0
  219. package/src/middlewares/elysia.ts +1 -2
  220. package/src/middlewares/express.test.ts +28 -0
  221. package/src/middlewares/express.ts +1 -1
  222. package/src/middlewares/hono.test.ts +138 -2
  223. package/src/middlewares/nextjs.test.ts +22 -0
  224. package/src/proxy/internal/Headers.test.ts +20 -0
  225. package/src/proxy/internal/Headers.ts +12 -1
  226. package/src/proxy/services/openai.test.ts +57 -1
  227. package/src/proxy/services/openai.ts +2 -0
  228. package/src/server/Methods.ts +1 -0
  229. package/src/server/Mppx.test.ts +244 -1
  230. package/src/server/Mppx.ts +124 -11
  231. package/src/server/NodeListener.test.ts +28 -1
  232. package/src/server/Transport.test.ts +19 -0
  233. package/src/server/Transport.ts +20 -0
  234. package/src/server/index.ts +1 -1
  235. package/src/stripe/server/internal/html.gen.ts +1 -1
  236. package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
  237. package/src/tempo/client/ChannelOps.test.ts +61 -7
  238. package/src/tempo/client/ChannelOps.ts +18 -7
  239. package/src/tempo/client/Charge.test.ts +126 -0
  240. package/src/tempo/client/Charge.ts +10 -6
  241. package/src/tempo/client/Session.test.ts +130 -1
  242. package/src/tempo/client/Session.ts +12 -19
  243. package/src/tempo/client/SessionManager.test.ts +69 -2
  244. package/src/tempo/client/SessionManager.ts +4 -4
  245. package/src/tempo/internal/account.ts +13 -0
  246. package/src/tempo/internal/fee-payer.test.ts +32 -2
  247. package/src/tempo/internal/fee-payer.ts +6 -2
  248. package/src/tempo/server/Charge.test.ts +69 -0
  249. package/src/tempo/server/Charge.ts +32 -0
  250. package/src/tempo/server/Session.test.ts +30 -0
  251. package/src/tempo/server/Session.ts +15 -16
  252. package/src/tempo/server/internal/html.gen.ts +1 -1
  253. package/src/tempo/session/Chain.test.ts +4 -4
  254. package/src/tempo/session/Chain.ts +10 -6
  255. package/src/tempo/session/Voucher.test.ts +230 -1
  256. package/src/tempo/session/Voucher.ts +96 -48
  257. package/src/tempo/session/Ws.test.ts +71 -0
  258. package/src/tempo/session/Ws.ts +13 -0
  259. package/src/x402/Assets.ts +65 -0
  260. package/src/x402/Exact.e2e.test.ts +448 -0
  261. package/src/x402/Header.test.ts +73 -0
  262. package/src/x402/Header.ts +34 -0
  263. package/src/x402/PublicInterface.test-d.ts +39 -0
  264. package/src/x402/Types.ts +248 -0
  265. package/src/x402/client/Exact.test.ts +180 -0
  266. package/src/x402/client/Exact.ts +198 -0
  267. package/src/x402/index.ts +5 -0
  268. package/src/x402/internal/ChallengeBrand.ts +14 -0
  269. package/src/x402/internal/RouteBinding.ts +18 -0
  270. package/src/x402/server/EvmCharge.ts +394 -0
  271. package/src/x402/server/Facilitator.test.ts +111 -0
  272. package/src/x402/server/Facilitator.ts +56 -0
@@ -0,0 +1,394 @@
1
+ import { isDeepStrictEqual } from 'node:util'
2
+
3
+ import { Bytes, Hash } from 'ox'
4
+ import { getAddress } from 'viem'
5
+
6
+ import * as BodyDigest from '../../BodyDigest.js'
7
+ import * as Challenge from '../../Challenge.js'
8
+ import type * as Credential from '../../Credential.js'
9
+ import * as Credential_ from '../../Credential.js'
10
+ import { VerificationFailedError } from '../../Errors.js'
11
+ import * as Types from '../../evm/Types.js'
12
+ import * as PaymentRequest from '../../PaymentRequest.js'
13
+ import * as Scope from '../../server/internal/scope.js'
14
+ import * as ServerTransport from '../../server/Transport.js'
15
+ import * as x402_Header from '../Header.js'
16
+ import * as x402_RouteBinding from '../internal/RouteBinding.js'
17
+ import * as x402_Types from '../Types.js'
18
+ import * as x402_Facilitator from './Facilitator.js'
19
+
20
+ const pendingX402Credential = Symbol('mppx.evm.pendingX402Credential')
21
+ const x402Credential = Symbol('mppx.evm.x402Credential')
22
+ const mppxExtensionKey = 'mppx'
23
+ const mppxRouteBindingSchema = {
24
+ additionalProperties: false,
25
+ properties: {
26
+ [Scope.reservedMetaKey]: { type: 'string' },
27
+ digest: { type: 'string' },
28
+ method: { type: 'string' },
29
+ nonce: { type: 'string' },
30
+ opaque: { type: 'string' },
31
+ },
32
+ required: ['method'],
33
+ type: 'object',
34
+ }
35
+
36
+ export type Options = {
37
+ /** Facilitator client or base URL used for x402-compatible settlement. */
38
+ facilitator?: string | x402_Types.Facilitator | undefined
39
+ /** Fetch implementation used for facilitator RPCs. */
40
+ fetch?: typeof globalThis.fetch | undefined
41
+ /** Maximum time in seconds allowed for x402-compatible payment completion. @default 300 */
42
+ maxTimeoutSeconds?: number | undefined
43
+ }
44
+
45
+ export type ResolvedOptions = {
46
+ authorization: Types.AuthorizationConfig
47
+ facilitator?: x402_Types.Facilitator | undefined
48
+ maxTimeoutSeconds: number
49
+ }
50
+
51
+ export type Path = {
52
+ bindCredential: NonNullable<ServerTransport.Http['bindCredential']>
53
+ getCredential: ServerTransport.Http['getCredential']
54
+ respondChallenge: (
55
+ options: Parameters<ServerTransport.Http['respondChallenge']>[0],
56
+ response?: Response | undefined,
57
+ ) => Response | Promise<Response>
58
+ respondReceipt: (
59
+ options: Parameters<ServerTransport.Http['respondReceipt']>[0],
60
+ response: Response,
61
+ ) => Response
62
+ }
63
+
64
+ /** Resolves optional x402 compatibility options for an EVM charge. */
65
+ export function resolveOptions(parameters: {
66
+ authorization: Types.AuthorizationConfig
67
+ options?: Options | undefined
68
+ }): ResolvedOptions {
69
+ return {
70
+ authorization: parameters.authorization,
71
+ ...(parameters.options?.facilitator
72
+ ? {
73
+ facilitator: x402_Facilitator.resolve(
74
+ parameters.options.facilitator,
75
+ 'EVM authorization x402 requires `facilitator`.',
76
+ { fetch: parameters.options.fetch },
77
+ ),
78
+ }
79
+ : {}),
80
+ maxTimeoutSeconds: parameters.options?.maxTimeoutSeconds ?? 300,
81
+ }
82
+ }
83
+
84
+ /** Creates the x402 wire path for an EVM charge method. */
85
+ export function createPath(config: ResolvedOptions): Path {
86
+ return {
87
+ getCredential(request) {
88
+ const paymentSignature = request.headers.get(x402_Types.paymentSignatureHeader)
89
+ if (!paymentSignature) return null
90
+ const paymentPayload = x402_Header.decodePaymentSignature(paymentSignature)
91
+
92
+ return markPendingCredential(
93
+ Credential_.from({
94
+ challenge: pendingChallenge(paymentPayload),
95
+ payload: paymentPayload,
96
+ }),
97
+ )
98
+ },
99
+
100
+ async bindCredential({ challenge, credential, input }) {
101
+ const paymentPayload = parsePaymentPayload(credential.payload)
102
+ if (!paymentPayload) return credential
103
+ if (!isPendingCredential(credential)) return credential
104
+ await assertBodyDigest(challenge, input)
105
+
106
+ const request = challenge.request as Types.ChargeRequest
107
+ const paymentRequirements = toPaymentRequirements(request, config)
108
+ if (!isDeepStrictEqual(paymentPayload.accepted, paymentRequirements))
109
+ throw new VerificationFailedError({
110
+ reason: 'x402 payment payload does not match route requirements',
111
+ })
112
+
113
+ const expectedResource = { url: input.url }
114
+ if (!isDeepStrictEqual(paymentPayload.resource, expectedResource))
115
+ throw new VerificationFailedError({
116
+ reason: 'x402 payment payload resource does not match route resource',
117
+ })
118
+
119
+ const expectedExtensions = routeExtensions(challenge, input)
120
+ if (!containsExtensions(paymentPayload.extensions, expectedExtensions))
121
+ throw new VerificationFailedError({
122
+ reason: 'x402 payment payload extensions do not match route binding',
123
+ })
124
+
125
+ const payload = payloadToAuthorization(paymentPayload)
126
+ const expectedNonce = x402_RouteBinding.nonce({
127
+ accepted: paymentRequirements,
128
+ extensions: paymentPayload.extensions!,
129
+ resource: expectedResource,
130
+ })
131
+ if (payload.nonce !== expectedNonce)
132
+ throw new VerificationFailedError({
133
+ reason: 'x402 authorization nonce does not match route binding',
134
+ })
135
+
136
+ return markCredential(
137
+ Credential_.from({
138
+ challenge,
139
+ payload,
140
+ source: Types.toSource({
141
+ address: getAddress(payload.from),
142
+ chainId: request.methodDetails.chainId,
143
+ }),
144
+ }),
145
+ )
146
+ },
147
+
148
+ respondChallenge(options, response) {
149
+ if (!response) throw new Error('x402 path requires a base challenge response.')
150
+ if (options.input.body !== null && options.challenge.digest === undefined) return response
151
+ const headers = new Headers(response.headers)
152
+ const request = options.challenge.request as Types.ChargeRequest
153
+ headers.set(
154
+ x402_Types.paymentRequiredHeader,
155
+ x402_Header.encodePaymentRequired({
156
+ accepts: [toPaymentRequirements(request, config)],
157
+ error:
158
+ options.error?.message ?? `${x402_Types.paymentSignatureHeader} header is required`,
159
+ extensions: routeExtensions(options.challenge, options.input),
160
+ resource: { url: options.input.url },
161
+ x402Version: 2,
162
+ }),
163
+ )
164
+ return new Response(response.body, {
165
+ headers,
166
+ status: response.status,
167
+ statusText: response.statusText,
168
+ })
169
+ },
170
+
171
+ respondReceipt(options, response) {
172
+ if (!options.input.headers.has(x402_Types.paymentSignatureHeader)) return response
173
+
174
+ const payload = Types.AuthorizationPayloadSchema.parse(options.credential.payload)
175
+ const request = options.credential.challenge.request as Types.ChargeRequest
176
+ const headers = new Headers(response.headers)
177
+ headers.set(
178
+ x402_Types.paymentResponseHeader,
179
+ x402_Header.encodePaymentResponse({
180
+ network: Types.networkOf(request.methodDetails.chainId),
181
+ payer: payload.from,
182
+ success: true,
183
+ transaction: options.receipt.reference,
184
+ }),
185
+ )
186
+ return new Response(response.body, {
187
+ headers,
188
+ status: response.status,
189
+ statusText: response.statusText,
190
+ })
191
+ },
192
+ }
193
+ }
194
+
195
+ /** Settles a verified EVM authorization through an x402 facilitator. */
196
+ export function settleWithFacilitator(parameters: ResolvedOptions): SettleWithFacilitator {
197
+ const { facilitator, maxTimeoutSeconds } = parameters
198
+ if (!facilitator) throw new Error('EVM authorization x402 requires `facilitator`.')
199
+
200
+ return async ({ payload, request }) => {
201
+ const paymentRequirements = toPaymentRequirements(request, {
202
+ ...parameters,
203
+ maxTimeoutSeconds,
204
+ })
205
+ const paymentPayload: x402_Types.PaymentPayload = {
206
+ accepted: paymentRequirements,
207
+ payload: {
208
+ authorization: {
209
+ from: payload.from,
210
+ nonce: payload.nonce,
211
+ to: payload.to,
212
+ validAfter: payload.validAfter,
213
+ validBefore: payload.validBefore,
214
+ value: payload.value,
215
+ },
216
+ signature: payload.signature,
217
+ },
218
+ x402Version: 2,
219
+ }
220
+
221
+ const verified = await facilitator.verify(paymentPayload, paymentRequirements)
222
+ if (!verified.isValid)
223
+ throw new VerificationFailedError({
224
+ reason:
225
+ verified.invalidMessage ?? verified.invalidReason ?? 'EVM facilitator verify failed',
226
+ })
227
+
228
+ const settled = await facilitator.settle(paymentPayload, paymentRequirements)
229
+ if (!settled.success)
230
+ throw new VerificationFailedError({
231
+ reason: settled.errorMessage ?? settled.errorReason ?? 'EVM facilitator settlement failed',
232
+ })
233
+
234
+ return {
235
+ reference: settled.transaction,
236
+ }
237
+ }
238
+ }
239
+
240
+ export type SettleWithFacilitator = (parameters: {
241
+ credential: Credential.Credential<Types.AuthorizationPayload>
242
+ payload: Types.AuthorizationPayload
243
+ request: Types.ChargeRequest
244
+ source: ReturnType<typeof Types.toSource>
245
+ }) => Promise<{
246
+ reference: string
247
+ timestamp?: string | undefined
248
+ }>
249
+
250
+ /** Returns whether a credential was converted from an x402 payment payload. */
251
+ export function isCredential(credential: Credential.Credential): boolean {
252
+ return (credential as { [x402Credential]?: true })[x402Credential] === true
253
+ }
254
+
255
+ /** Returns whether a credential was parsed from the x402 payment header. */
256
+ export function isPendingCredential(credential: Credential.Credential): boolean {
257
+ return (credential as { [pendingX402Credential]?: true })[pendingX402Credential] === true
258
+ }
259
+
260
+ /** Converts a native EVM charge request to x402 exact payment requirements. */
261
+ export function toPaymentRequirements(
262
+ request: Types.ChargeRequest,
263
+ config: Pick<ResolvedOptions, 'authorization' | 'maxTimeoutSeconds'>,
264
+ ): x402_Types.PaymentRequirements {
265
+ return {
266
+ amount: request.amount,
267
+ asset: request.currency,
268
+ extra: {
269
+ assetTransferMethod: Types.eip3009,
270
+ name: config.authorization.name,
271
+ version: config.authorization.version,
272
+ },
273
+ maxTimeoutSeconds: config.maxTimeoutSeconds,
274
+ network: Types.networkOf(request.methodDetails.chainId),
275
+ payTo: request.recipient,
276
+ scheme: 'exact',
277
+ }
278
+ }
279
+
280
+ function parsePaymentPayload(payload: unknown): x402_Types.PaymentPayload | undefined {
281
+ const parsed = x402_Types.PaymentPayloadSchema.safeParse(payload)
282
+ return parsed.success ? parsed.data : undefined
283
+ }
284
+
285
+ /** Converts an x402 EIP-3009 payment payload to the native EVM authorization payload. */
286
+ export function payloadToAuthorization(
287
+ paymentPayload: x402_Types.PaymentPayload,
288
+ ): Types.AuthorizationPayload {
289
+ if (!('authorization' in paymentPayload.payload))
290
+ throw new VerificationFailedError({
291
+ reason: 'EVM charge only supports x402 EIP-3009 authorization payloads',
292
+ })
293
+
294
+ return Types.AuthorizationPayloadSchema.parse({
295
+ ...paymentPayload.payload.authorization,
296
+ signature: paymentPayload.payload.signature,
297
+ type: 'authorization',
298
+ })
299
+ }
300
+
301
+ function pendingChallenge(paymentPayload: x402_Types.PaymentPayload) {
302
+ // The route challenge is built after request normalization in bindCredential().
303
+ // Until then, this deterministic local ID only carries the x402 payload through
304
+ // the standard credential pipeline; it is never HMAC-verified.
305
+ return Challenge.from({
306
+ id: pendingChallengeId(paymentPayload),
307
+ intent: Types.chargeIntent,
308
+ method: Types.paymentMethod,
309
+ realm: 'x402',
310
+ request: paymentPayload.accepted,
311
+ })
312
+ }
313
+
314
+ function pendingChallengeId(paymentPayload: x402_Types.PaymentPayload): string {
315
+ const hash = Hash.sha256(Bytes.fromString(JSON.stringify(paymentPayload)), { as: 'Hex' })
316
+ return `${x402_Types.syntheticChallengeIdPrefix}${hash}`
317
+ }
318
+
319
+ function routeExtensions(challenge: Challenge.Challenge, input: Request): x402_Types.Extensions {
320
+ const binding: Record<string, unknown> = { method: input.method }
321
+ const scope = Scope.read(challenge.meta)
322
+ if (scope !== undefined) binding[Scope.reservedMetaKey] = scope
323
+ if (challenge.digest !== undefined) binding.digest = challenge.digest
324
+ const opaque =
325
+ challenge.opaque ?? (challenge.meta ? PaymentRequest.serialize(challenge.meta) : undefined)
326
+ if (opaque !== undefined) binding.opaque = opaque
327
+ return {
328
+ [mppxExtensionKey]: {
329
+ info: binding,
330
+ schema: mppxRouteBindingSchema,
331
+ },
332
+ }
333
+ }
334
+
335
+ function containsExtensions(
336
+ actual: x402_Types.Extensions | undefined,
337
+ expected: x402_Types.Extensions,
338
+ ): boolean {
339
+ if (!actual) return false
340
+ return Object.entries(expected).every(([key, expectedExtension]) => {
341
+ const actualExtension = actual[key]
342
+ return (
343
+ actualExtension !== undefined &&
344
+ isDeepStrictEqual(actualExtension.schema, expectedExtension.schema) &&
345
+ isDeepStrictEqual(stripClientNonce(actualExtension.info), expectedExtension.info)
346
+ )
347
+ })
348
+ }
349
+
350
+ function stripClientNonce(info: Record<string, unknown>): Record<string, unknown> {
351
+ const { nonce, ...rest } = info
352
+ if (nonce !== undefined && typeof nonce !== 'string') return info
353
+ return rest
354
+ }
355
+
356
+ async function assertBodyDigest(challenge: Challenge.Challenge, input: Request): Promise<void> {
357
+ if (input.body === null) return
358
+ if (challenge.digest === undefined)
359
+ throw new VerificationFailedError({
360
+ reason: 'x402 payment requires a body digest for body-bearing requests',
361
+ })
362
+ let body: string
363
+ try {
364
+ body = await input.clone().text()
365
+ } catch {
366
+ throw new VerificationFailedError({
367
+ reason: 'x402 payment cannot bind streaming request body',
368
+ })
369
+ }
370
+ if (!BodyDigest.verify(challenge.digest as BodyDigest.BodyDigest, body))
371
+ throw new VerificationFailedError({
372
+ reason: 'x402 payment body digest mismatch',
373
+ })
374
+ }
375
+
376
+ function markPendingCredential<const credential extends Credential.Credential>(
377
+ credential: credential,
378
+ ): credential {
379
+ Object.defineProperty(credential, pendingX402Credential, {
380
+ enumerable: true,
381
+ value: true,
382
+ })
383
+ return credential
384
+ }
385
+
386
+ function markCredential<const credential extends Credential.Credential>(
387
+ credential: credential,
388
+ ): credential {
389
+ Object.defineProperty(credential, x402Credential, {
390
+ enumerable: true,
391
+ value: true,
392
+ })
393
+ return credential
394
+ }
@@ -0,0 +1,111 @@
1
+ import { afterEach, describe, expect, test, vi } from 'vp/test'
2
+
3
+ import type * as Types from '../Types.js'
4
+ import * as Facilitator from './Facilitator.js'
5
+
6
+ const paymentRequirements = {
7
+ amount: '10000',
8
+ asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
9
+ extra: {
10
+ assetTransferMethod: 'eip3009',
11
+ name: 'USDC',
12
+ version: '2',
13
+ },
14
+ maxTimeoutSeconds: 60,
15
+ network: 'eip155:84532',
16
+ payTo: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
17
+ scheme: 'exact',
18
+ } satisfies Types.PaymentRequirements
19
+
20
+ const paymentPayload = {
21
+ accepted: paymentRequirements,
22
+ payload: {
23
+ authorization: {
24
+ from: '0x857b06519E91e3A54538791bDbb0E22373e36b66',
25
+ nonce: '0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480',
26
+ to: paymentRequirements.payTo,
27
+ validAfter: '1740672089',
28
+ validBefore: '1740672154',
29
+ value: paymentRequirements.amount,
30
+ },
31
+ signature:
32
+ '0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c',
33
+ },
34
+ x402Version: 2,
35
+ } satisfies Types.PaymentPayload
36
+
37
+ afterEach(() => {
38
+ vi.unstubAllGlobals()
39
+ })
40
+
41
+ describe('x402 facilitator http client', () => {
42
+ test('sends v2 verify envelopes', async () => {
43
+ const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
44
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
45
+ calls.push({ init, input })
46
+ return new Response(JSON.stringify({ isValid: true }), {
47
+ headers: { 'Content-Type': 'application/json' },
48
+ })
49
+ })
50
+ vi.stubGlobal('fetch', fetchMock)
51
+
52
+ const facilitator = Facilitator.http('https://facilitator.example')
53
+ await facilitator.verify(paymentPayload, paymentRequirements)
54
+
55
+ expect(fetchMock).toHaveBeenCalledOnce()
56
+ const { init } = calls[0]!
57
+ expect(JSON.parse(init!.body as string)).toEqual({
58
+ paymentPayload,
59
+ paymentRequirements,
60
+ x402Version: 2,
61
+ })
62
+ })
63
+
64
+ test('sends v2 settle envelopes', async () => {
65
+ const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
66
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
67
+ calls.push({ init, input })
68
+ return new Response(
69
+ JSON.stringify({
70
+ network: paymentRequirements.network,
71
+ success: true,
72
+ transaction: `0x${'1'.repeat(64)}`,
73
+ }),
74
+ { headers: { 'Content-Type': 'application/json' } },
75
+ )
76
+ })
77
+ vi.stubGlobal('fetch', fetchMock)
78
+
79
+ const facilitator = Facilitator.http('https://facilitator.example/')
80
+ await facilitator.settle(paymentPayload, paymentRequirements)
81
+
82
+ expect(fetchMock).toHaveBeenCalledOnce()
83
+ expect(calls[0]!.input).toBe('https://facilitator.example/settle')
84
+ const { init } = calls[0]!
85
+ expect(JSON.parse(init!.body as string)).toEqual({
86
+ paymentPayload,
87
+ paymentRequirements,
88
+ x402Version: 2,
89
+ })
90
+ })
91
+
92
+ test('unwraps mppx fetch wrappers', async () => {
93
+ const rawFetch = vi.fn(async () => {
94
+ return new Response(JSON.stringify({ isValid: true }), {
95
+ headers: { 'Content-Type': 'application/json' },
96
+ })
97
+ })
98
+ const wrappedFetch = vi.fn(async () => {
99
+ throw new Error('wrapped fetch should not be called')
100
+ }) as unknown as typeof globalThis.fetch & {
101
+ [key: symbol]: typeof globalThis.fetch
102
+ }
103
+ wrappedFetch[Symbol.for('mppx.fetch.wrapper')] = rawFetch as typeof globalThis.fetch
104
+
105
+ const facilitator = Facilitator.http('https://facilitator.example', { fetch: wrappedFetch })
106
+ await facilitator.verify(paymentPayload, paymentRequirements)
107
+
108
+ expect(wrappedFetch).not.toHaveBeenCalled()
109
+ expect(rawFetch).toHaveBeenCalledOnce()
110
+ })
111
+ })
@@ -0,0 +1,56 @@
1
+ import * as Types from '../Types.js'
2
+
3
+ const mppxFetchWrapper = Symbol.for('mppx.fetch.wrapper')
4
+
5
+ type WrappedFetch = typeof globalThis.fetch & {
6
+ [mppxFetchWrapper]?: typeof globalThis.fetch
7
+ }
8
+
9
+ export type HttpOptions = {
10
+ /** Fetch implementation used for facilitator RPCs. */
11
+ fetch?: typeof globalThis.fetch | undefined
12
+ }
13
+
14
+ /** Resolves an x402 facilitator URL or client into a facilitator client. */
15
+ export function resolve(
16
+ facilitator: string | Types.Facilitator,
17
+ errorMessage = 'x402 exact requires `facilitator`.',
18
+ options?: HttpOptions | undefined,
19
+ ): Types.Facilitator {
20
+ if (typeof facilitator === 'object' && facilitator !== null) return facilitator
21
+ if (typeof facilitator === 'string') return http(facilitator, options)
22
+ throw new Error(errorMessage)
23
+ }
24
+
25
+ /** Creates an x402 facilitator client from an HTTP base URL. */
26
+ export function http(url: string, options?: HttpOptions | undefined): Types.Facilitator {
27
+ const base = url.replace(/\/$/, '')
28
+ const fetch = unwrapFetch(options?.fetch ?? globalThis.fetch)
29
+ return {
30
+ async verify(paymentPayload, paymentRequirements) {
31
+ const response = await fetch(`${base}/verify`, {
32
+ body: JSON.stringify({ paymentPayload, paymentRequirements, x402Version: 2 }),
33
+ headers: { 'Content-Type': 'application/json' },
34
+ method: 'POST',
35
+ })
36
+ return Types.VerifyResponseSchema.parse(await response.json())
37
+ },
38
+ async settle(paymentPayload, paymentRequirements) {
39
+ const response = await fetch(`${base}/settle`, {
40
+ body: JSON.stringify({ paymentPayload, paymentRequirements, x402Version: 2 }),
41
+ headers: { 'Content-Type': 'application/json' },
42
+ method: 'POST',
43
+ })
44
+ return Types.SettleResponseSchema.parse(await response.json())
45
+ },
46
+ }
47
+ }
48
+
49
+ /** Returns the underlying raw fetch implementation when given an mppx wrapper. */
50
+ export function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch {
51
+ let current = fetch as WrappedFetch
52
+ while (current[mppxFetchWrapper]) {
53
+ current = current[mppxFetchWrapper] as WrappedFetch
54
+ }
55
+ return current as typeof globalThis.fetch
56
+ }