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
@@ -665,8 +665,37 @@ describe('Fetch.from: 402 retry path', () => {
665
665
  await fetch('https://example.com/api')
666
666
 
667
667
  const retryInit = calls[1]!.init as Record<string, unknown>
668
- const headers = retryInit.headers as Record<string, string>
669
- expect(headers.Authorization).toBe('credential')
668
+ const headers = new Headers(retryInit.headers as HeadersInit)
669
+ expect(headers.get('Authorization')).toBe('credential')
670
+ })
671
+
672
+ test('sends credential retry to the final 402 response URL', async () => {
673
+ let callCount = 0
674
+ const calls: { input: RequestInfo | URL; init: RequestInit | undefined }[] = []
675
+ const mockFetch: typeof globalThis.fetch = async (input, init) => {
676
+ calls.push({ input, init })
677
+ callCount++
678
+ if (callCount === 1) {
679
+ const response = make402()
680
+ Object.defineProperty(response, 'url', {
681
+ value: 'https://payments.example.com/protected',
682
+ })
683
+ return response
684
+ }
685
+ return new Response('OK', { status: 200 })
686
+ }
687
+
688
+ const fetch = Fetch.from({
689
+ fetch: mockFetch,
690
+ methods: [noopMethod],
691
+ })
692
+
693
+ const response = await fetch('https://api.example.com/protected')
694
+
695
+ expect(response.status).toBe(200)
696
+ expect(calls[0]!.input).toBe('https://api.example.com/protected')
697
+ expect(calls[1]!.input).toBe('https://payments.example.com/protected')
698
+ expect(new Headers(calls[1]!.init?.headers).get('Authorization')).toBe('credential')
670
699
  })
671
700
 
672
701
  test('emits client events and allows challenge handler to provide credential', async () => {
@@ -4,6 +4,7 @@ import * as AcceptPayment from '../../internal/AcceptPayment.js'
4
4
  import type { MaybePromise } from '../../internal/types.js'
5
5
  import type * as Method from '../../Method.js'
6
6
  import type * as z from '../../zod.js'
7
+ import * as Transport from '../Transport.js'
7
8
 
8
9
  // We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
9
10
  // even across multiple module instances/bundles. This lets restore() avoid clobbering
@@ -162,6 +163,7 @@ export function from<const methods extends readonly Method.AnyClient[]>(
162
163
  methods,
163
164
  onChallenge,
164
165
  orderChallenges,
166
+ transport = Transport.http(),
165
167
  } = config
166
168
  const events = config.eventDispatcher ?? createEventDispatcher()
167
169
  const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods)
@@ -183,7 +185,7 @@ export function from<const methods extends readonly Method.AnyClient[]>(
183
185
  )
184
186
  const response = await baseFetch(initialRequest.input, initialRequest.init)
185
187
 
186
- if (response.status !== 402) return response
188
+ if (!transport.isPaymentRequired(response)) return response
187
189
 
188
190
  // Only extract context for payment handling after confirming 402.
189
191
  const context = (init as Record<string, unknown> | undefined)?.context
@@ -198,8 +200,9 @@ export function from<const methods extends readonly Method.AnyClient[]>(
198
200
  let mi: methods[number] | undefined
199
201
 
200
202
  try {
201
- // Parse all challenges from the response (supports merged WWW-Authenticate headers).
202
- challenges = Challenge.fromResponseList(response)
203
+ challenges = transport.getChallenges
204
+ ? transport.getChallenges(response)
205
+ : [transport.getChallenge(response)]
203
206
 
204
207
  const candidates = AcceptPayment.selectChallengeCandidates(
205
208
  challenges,
@@ -258,10 +261,17 @@ export function from<const methods extends readonly Method.AnyClient[]>(
258
261
  }),
259
262
  )
260
263
 
261
- const paymentResponse = await baseFetch(initialRequest.input, {
262
- ...fetchInit,
263
- headers: withAuthorizationHeader(initialRequest.headers, credential),
264
- })
264
+ const paymentResponse = await baseFetch(
265
+ resolvePaymentRetryInput(response, initialRequest.input),
266
+ transport.setCredential(
267
+ {
268
+ ...fetchInit,
269
+ headers: initialRequest.headers,
270
+ },
271
+ credential,
272
+ { challenge: selectedChallenge },
273
+ ),
274
+ )
265
275
  if (paymentResponse.ok)
266
276
  await events.emit(
267
277
  'payment.response',
@@ -335,6 +345,8 @@ export declare namespace from {
335
345
  | undefined
336
346
  /** Filters and sorts supported challenges before credential creation. */
337
347
  orderChallenges?: AcceptPayment.OrderChallenges<methods> | undefined
348
+ /** Transport to use for challenge extraction and credential attachment. */
349
+ transport?: Transport.AnyTransport | undefined
338
350
  }
339
351
 
340
352
  type Fetch<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = (
@@ -688,18 +700,6 @@ function freezeSnapshot<value>(value: value): value {
688
700
  return value
689
701
  }
690
702
 
691
- /** @internal */
692
- function withAuthorizationHeader(headers: unknown, credential: string): Record<string, string> {
693
- const normalized = normalizeHeaders(headers)
694
- // Remove any existing Authorization header regardless of casing to avoid
695
- // duplicate/conflicting credentials on retry.
696
- for (const key of Object.keys(normalized)) {
697
- if (key.toLowerCase() === 'authorization') delete normalized[key]
698
- }
699
- normalized.Authorization = credential
700
- return normalized
701
- }
702
-
703
703
  /** @internal */
704
704
  function prepareInitialRequest<methods extends readonly Method.AnyClient[]>(
705
705
  input: RequestInfo | URL,
@@ -843,3 +843,10 @@ function resolveRequestUrl(input: RequestInfo | URL): URL {
843
843
  if (input instanceof Request) return new URL(input.url)
844
844
  return new URL(input, isBrowser() ? globalThis.location.href : undefined)
845
845
  }
846
+
847
+ function resolvePaymentRetryInput(
848
+ response: Response,
849
+ fallback: RequestInfo | URL,
850
+ ): RequestInfo | URL {
851
+ return response.url ? response.url : fallback
852
+ }
@@ -0,0 +1 @@
1
+ export * from '../x402/Assets.js'
@@ -0,0 +1,5 @@
1
+ /** Base mainnet EVM chain ID. */
2
+ export const base = 8453
3
+
4
+ /** Base Sepolia testnet EVM chain ID. */
5
+ export const baseSepolia = 84532
@@ -0,0 +1,44 @@
1
+ import { getAddress, parseUnits } from 'viem'
2
+
3
+ import * as Method from '../Method.js'
4
+ import * as z from '../zod.js'
5
+ import * as Types from './Types.js'
6
+
7
+ /** Native Payment-auth EVM charge method. */
8
+ export const charge = Method.from({
9
+ name: Types.paymentMethod,
10
+ intent: Types.chargeIntent,
11
+ schema: {
12
+ credential: {
13
+ payload: Types.ChargePayloadSchema,
14
+ },
15
+ request: z.pipe(
16
+ Types.ChargeRequestInputSchema,
17
+ z.transform(
18
+ ({
19
+ amount,
20
+ chainId,
21
+ credentialTypes = ['authorization'],
22
+ currency,
23
+ decimals,
24
+ permit2Address,
25
+ recipient,
26
+ splits,
27
+ ...request
28
+ }) => ({
29
+ ...request,
30
+ amount: parseUnits(amount, decimals).toString(),
31
+ currency: getAddress(currency),
32
+ methodDetails: {
33
+ chainId,
34
+ credentialTypes,
35
+ decimals,
36
+ ...(permit2Address ? { permit2Address: getAddress(permit2Address) } : {}),
37
+ ...(splits ? { splits } : {}),
38
+ },
39
+ recipient: getAddress(recipient),
40
+ }),
41
+ ),
42
+ ),
43
+ },
44
+ })
@@ -0,0 +1,109 @@
1
+ import * as evmRoot from 'mppx/evm'
2
+ import {
3
+ assets as clientAssets,
4
+ chains as clientChains,
5
+ charge as clientCharge,
6
+ evm as clientEvm,
7
+ } from 'mppx/evm/client'
8
+ import { assets as serverAssets, charge as serverCharge, evm as serverEvm } from 'mppx/evm/server'
9
+ import type { Account } from 'viem'
10
+ import { describe, expectTypeOf, test } from 'vp/test'
11
+
12
+ import { Mppx as ClientMppx } from '../client/index.js'
13
+ import { Mppx as ServerMppx } from '../server/index.js'
14
+
15
+ const account = {} as Account
16
+ const recipient = '0x209693Bc6afc0C5328bA36FaF03C514EF312287C'
17
+ const secretKey = 'test-secret'
18
+ const settle = async () => ({
19
+ reference: `0x${'1'.repeat(64)}` as `0x${string}`,
20
+ })
21
+ const facilitator = {
22
+ settle: async () => ({
23
+ network: 'eip155:8453',
24
+ success: true,
25
+ transaction: `0x${'1'.repeat(64)}` as `0x${string}`,
26
+ }),
27
+ verify: async () => ({ isValid: true }),
28
+ }
29
+
30
+ describe('evm public interface', () => {
31
+ test('exports EVM asset metadata from root and subpaths', () => {
32
+ expectTypeOf(evmRoot.assets.base.USDC).toMatchTypeOf<typeof serverAssets.base.USDC>()
33
+ expectTypeOf(clientAssets.baseSepolia.USDC).toMatchTypeOf<
34
+ typeof serverAssets.baseSepolia.USDC
35
+ >()
36
+ expectTypeOf(evmRoot.chains.base).toMatchTypeOf<number>()
37
+ expectTypeOf(clientChains.baseSepolia).toMatchTypeOf<number>()
38
+ })
39
+
40
+ test('server charge works through subpath exports and tuple helper', () => {
41
+ const direct = serverCharge({
42
+ currency: serverAssets.base.USDC,
43
+ recipient,
44
+ x402: { facilitator },
45
+ })
46
+ expectTypeOf(direct.name).toEqualTypeOf<'evm'>()
47
+ expectTypeOf(direct.intent).toEqualTypeOf<'charge'>()
48
+
49
+ const mppx = ServerMppx.create({
50
+ methods: [
51
+ serverEvm({
52
+ currency: serverAssets.base.USDC,
53
+ recipient,
54
+ x402: { facilitator },
55
+ }),
56
+ ],
57
+ secretKey,
58
+ })
59
+
60
+ expectTypeOf(mppx.evm.charge).toBeFunction()
61
+ expectTypeOf(mppx.evm.charge({ amount: '0.01' })).toBeFunction()
62
+ })
63
+
64
+ test('server charge accepts a custom settlement override', () => {
65
+ const direct = serverCharge({
66
+ currency: serverAssets.base.USDC,
67
+ recipient,
68
+ settle,
69
+ })
70
+ expectTypeOf(direct.name).toEqualTypeOf<'evm'>()
71
+ expectTypeOf(direct.intent).toEqualTypeOf<'charge'>()
72
+ })
73
+
74
+ test('client charge works through subpath exports and tuple helper', () => {
75
+ const direct = clientCharge({
76
+ account,
77
+ currencies: [clientAssets.baseSepolia.USDC],
78
+ maxAmount: '0.01',
79
+ networks: [clientChains.baseSepolia],
80
+ })
81
+ expectTypeOf(direct.name).toEqualTypeOf<'evm'>()
82
+ expectTypeOf(direct.intent).toEqualTypeOf<'charge'>()
83
+
84
+ const mppx = ClientMppx.create({
85
+ methods: [
86
+ clientEvm({
87
+ account,
88
+ currencies: [clientAssets.baseSepolia.USDC],
89
+ maxAmount: '0.01',
90
+ networks: [clientEvm.chains.baseSepolia],
91
+ }),
92
+ ],
93
+ polyfill: false,
94
+ })
95
+
96
+ expectTypeOf(mppx.createCredential).toBeFunction()
97
+ })
98
+
99
+ test('server charge rejects x402 exact config shape', () => {
100
+ serverCharge({
101
+ // @ts-expect-error evm.charge takes shared charge config, not x402 exact config.
102
+ config: {
103
+ currency: serverAssets.base.USDC,
104
+ recipient,
105
+ x402: { facilitator },
106
+ },
107
+ })
108
+ })
109
+ })
@@ -0,0 +1,140 @@
1
+ import { Bytes, Hash } from 'ox'
2
+
3
+ import * as z from '../zod.js'
4
+
5
+ /** Payment method name for Payment-auth EVM challenges. */
6
+ export const paymentMethod = 'evm' as const
7
+
8
+ /** Payment intent name for one-time EVM charges. */
9
+ export const chargeIntent = 'charge' as const
10
+
11
+ /** CAIP-2 namespace prefix for EVM networks. */
12
+ export const evmNetworkPrefix = 'eip155:' as const
13
+
14
+ /** EIP-3009 transfer authorization method identifier. */
15
+ export const eip3009 = 'eip3009' as const
16
+
17
+ /** EVM charge credential types supported by this package. */
18
+ export const credentialTypes = ['authorization'] as const
19
+
20
+ const atomicAmount = z.string().check(z.regex(/^\d+$/, 'Invalid atomic amount'))
21
+
22
+ /** EIP-3009 domain metadata for `transferWithAuthorization` signatures. */
23
+ export const AuthorizationConfigSchema = z.object({
24
+ name: z.string().check(z.minLength(1)),
25
+ version: z.string().check(z.minLength(1)),
26
+ })
27
+ export type AuthorizationConfig = z.infer<typeof AuthorizationConfigSchema>
28
+
29
+ /** CAIP-2 EVM network identifier. */
30
+ export type EvmNetwork = `${typeof evmNetworkPrefix}${number}`
31
+
32
+ /** EVM-specific charge method details. */
33
+ export const MethodDetailsSchema = z.object({
34
+ chainId: z.number(),
35
+ credentialTypes: z.optional(z.array(z.enum(credentialTypes)).check(z.minLength(1))),
36
+ decimals: z.optional(z.number()),
37
+ permit2Address: z.optional(z.address()),
38
+ splits: z.optional(
39
+ z.array(
40
+ z.object({
41
+ amount: atomicAmount,
42
+ recipient: z.address(),
43
+ }),
44
+ ),
45
+ ),
46
+ })
47
+ export type MethodDetails = z.infer<typeof MethodDetailsSchema>
48
+
49
+ /** Canonical Payment-auth EVM charge request. */
50
+ export const ChargeRequestSchema = z.object({
51
+ amount: atomicAmount,
52
+ currency: z.address(),
53
+ description: z.optional(z.string()),
54
+ externalId: z.optional(z.string()),
55
+ methodDetails: MethodDetailsSchema,
56
+ recipient: z.address(),
57
+ })
58
+ export type ChargeRequest = z.infer<typeof ChargeRequestSchema>
59
+
60
+ /** Public route input before display-unit amounts are converted to atomic units. */
61
+ export const ChargeRequestInputSchema = z.object({
62
+ amount: z.amount(),
63
+ chainId: z.number(),
64
+ currency: z.address(),
65
+ credentialTypes: z.optional(z.array(z.enum(credentialTypes)).check(z.minLength(1))),
66
+ decimals: z.number(),
67
+ description: z.optional(z.string()),
68
+ externalId: z.optional(z.string()),
69
+ permit2Address: z.optional(z.address()),
70
+ recipient: z.address(),
71
+ splits: z.optional(
72
+ z.array(
73
+ z.object({
74
+ amount: atomicAmount,
75
+ recipient: z.address(),
76
+ }),
77
+ ),
78
+ ),
79
+ })
80
+ export type ChargeRequestInput = z.infer<typeof ChargeRequestInputSchema>
81
+
82
+ /** Payment-auth EVM authorization credential payload. */
83
+ export const AuthorizationPayloadSchema = z.object({
84
+ from: z.address(),
85
+ nonce: z.hash(),
86
+ signature: z.signature(),
87
+ to: z.address(),
88
+ type: z.literal('authorization'),
89
+ validAfter: atomicAmount,
90
+ validBefore: atomicAmount,
91
+ value: atomicAmount,
92
+ })
93
+ export type AuthorizationPayload = z.infer<typeof AuthorizationPayloadSchema>
94
+
95
+ /** Payment-auth EVM charge credential payload. */
96
+ export const ChargePayloadSchema = AuthorizationPayloadSchema
97
+ export type ChargePayload = z.infer<typeof ChargePayloadSchema>
98
+
99
+ /** EIP-712 type definition for EIP-3009 `transferWithAuthorization`. */
100
+ export const authorizationTypes = {
101
+ TransferWithAuthorization: [
102
+ { name: 'from', type: 'address' },
103
+ { name: 'to', type: 'address' },
104
+ { name: 'value', type: 'uint256' },
105
+ { name: 'validAfter', type: 'uint256' },
106
+ { name: 'validBefore', type: 'uint256' },
107
+ { name: 'nonce', type: 'bytes32' },
108
+ ],
109
+ } as const
110
+
111
+ /** Returns the EIP-712 domain for an EIP-3009 authorization signature. */
112
+ export function authorizationDomain(parameters: {
113
+ authorization: AuthorizationConfig
114
+ chainId: number
115
+ currency: `0x${string}`
116
+ }) {
117
+ return {
118
+ chainId: parameters.chainId,
119
+ name: parameters.authorization.name,
120
+ verifyingContract: parameters.currency,
121
+ version: parameters.authorization.version,
122
+ } as const
123
+ }
124
+
125
+ /** Computes the Payment-auth EVM challenge hash used as the EIP-3009 nonce. */
126
+ export function challengeHash(challenge: { id: string; realm: string }): `0x${string}` {
127
+ return Hash.keccak256(Bytes.fromString(`${challenge.id}${challenge.realm}`), {
128
+ as: 'Hex',
129
+ }) as `0x${string}`
130
+ }
131
+
132
+ /** Converts a chain ID to the CAIP-2 EVM network identifier. */
133
+ export function networkOf(chainId: number): EvmNetwork {
134
+ return `${evmNetworkPrefix}${chainId}`
135
+ }
136
+
137
+ /** Formats an EVM address as a did:pkh source identifier. */
138
+ export function toSource(parameters: { address: `0x${string}`; chainId: number }) {
139
+ return `did:pkh:eip155:${parameters.chainId}:${parameters.address}` as const
140
+ }
@@ -0,0 +1,99 @@
1
+ import { Challenge } from 'mppx'
2
+ import type { Account } from 'viem'
3
+ import { describe, expect, test, vi } from 'vp/test'
4
+
5
+ import * as Assets from '../Assets.js'
6
+ import * as Chains from '../Chains.js'
7
+ import { charge } from './Charge.js'
8
+
9
+ const account = {
10
+ address: '0x1111111111111111111111111111111111111111',
11
+ signTypedData: vi.fn(async () => '0x1234'),
12
+ } as unknown as Account
13
+
14
+ describe('evm charge client', () => {
15
+ test('does not sign x402 payloads from unbranded Payment-auth challenges', async () => {
16
+ const signTypedData = vi.fn(async () => '0x1234')
17
+ const client = charge({
18
+ account: {
19
+ ...account,
20
+ signTypedData,
21
+ } as unknown as Account,
22
+ currencies: [Assets.baseSepolia.USDC],
23
+ maxAmount: '0.01',
24
+ networks: [Chains.baseSepolia],
25
+ })
26
+ const challenge = Challenge.from({
27
+ id: 'attacker-controlled',
28
+ intent: 'charge',
29
+ method: 'evm',
30
+ realm: 'api.example.com',
31
+ request: {
32
+ amount: '10000',
33
+ asset: Assets.baseSepolia.USDC.address,
34
+ maxTimeoutSeconds: 60,
35
+ network: 'eip155:84532',
36
+ payTo: '0x2222222222222222222222222222222222222222',
37
+ scheme: 'exact',
38
+ },
39
+ })
40
+
41
+ await expect(client.createCredential({ challenge } as never)).rejects.toThrow()
42
+ expect(signTypedData).not.toHaveBeenCalled()
43
+ })
44
+
45
+ test('does not use server-supplied decimals for maxAmount policy', async () => {
46
+ const client = charge({
47
+ account,
48
+ maxAmount: '1',
49
+ })
50
+ const challenge = Challenge.from({
51
+ id: 'native',
52
+ intent: 'charge',
53
+ method: 'evm',
54
+ realm: 'api.example.com',
55
+ request: {
56
+ amount: '1000000000000000000',
57
+ currency: Assets.baseSepolia.USDC.address,
58
+ methodDetails: {
59
+ chainId: 84532,
60
+ credentialTypes: ['authorization'],
61
+ decimals: 18,
62
+ },
63
+ recipient: '0x2222222222222222222222222222222222222222',
64
+ },
65
+ })
66
+
67
+ await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
68
+ 'EVM charge maxAmount requires currency decimals.',
69
+ )
70
+ })
71
+
72
+ test('requires network policy for raw hex currencies', async () => {
73
+ const client = charge({
74
+ account,
75
+ currencies: [Assets.baseSepolia.USDC.address],
76
+ maxAmount: '1',
77
+ })
78
+ const challenge = Challenge.from({
79
+ id: 'native',
80
+ intent: 'charge',
81
+ method: 'evm',
82
+ realm: 'api.example.com',
83
+ request: {
84
+ amount: '1000000',
85
+ currency: Assets.baseSepolia.USDC.address,
86
+ methodDetails: {
87
+ chainId: 84532,
88
+ credentialTypes: ['authorization'],
89
+ decimals: 6,
90
+ },
91
+ recipient: '0x2222222222222222222222222222222222222222',
92
+ },
93
+ })
94
+
95
+ await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
96
+ 'EVM raw currency allowlists require networks.',
97
+ )
98
+ })
99
+ })