mppx 0.6.27 → 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 (295) hide show
  1. package/CHANGELOG.md +25 -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/Store.d.ts +32 -9
  8. package/dist/Store.d.ts.map +1 -1
  9. package/dist/Store.js +42 -10
  10. package/dist/Store.js.map +1 -1
  11. package/dist/client/Methods.d.ts +1 -0
  12. package/dist/client/Methods.d.ts.map +1 -1
  13. package/dist/client/Methods.js +1 -0
  14. package/dist/client/Methods.js.map +1 -1
  15. package/dist/client/Mppx.d.ts +3 -3
  16. package/dist/client/Mppx.d.ts.map +1 -1
  17. package/dist/client/Mppx.js +1 -0
  18. package/dist/client/Mppx.js.map +1 -1
  19. package/dist/client/Transport.d.ts +10 -3
  20. package/dist/client/Transport.d.ts.map +1 -1
  21. package/dist/client/Transport.js +60 -7
  22. package/dist/client/Transport.js.map +1 -1
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +1 -1
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/internal/Fetch.d.ts +3 -0
  28. package/dist/client/internal/Fetch.d.ts.map +1 -1
  29. package/dist/client/internal/Fetch.js +12 -20
  30. package/dist/client/internal/Fetch.js.map +1 -1
  31. package/dist/evm/Assets.d.ts +2 -0
  32. package/dist/evm/Assets.d.ts.map +1 -0
  33. package/dist/evm/Assets.js +2 -0
  34. package/dist/evm/Assets.js.map +1 -0
  35. package/dist/evm/Chains.d.ts +5 -0
  36. package/dist/evm/Chains.d.ts.map +1 -0
  37. package/dist/evm/Chains.js +5 -0
  38. package/dist/evm/Chains.js.map +1 -0
  39. package/dist/evm/Methods.d.ts +68 -0
  40. package/dist/evm/Methods.d.ts.map +1 -0
  41. package/dist/evm/Methods.js +28 -0
  42. package/dist/evm/Methods.js.map +1 -0
  43. package/dist/evm/Types.d.ts +143 -0
  44. package/dist/evm/Types.d.ts.map +1 -0
  45. package/dist/evm/Types.js +102 -0
  46. package/dist/evm/Types.js.map +1 -0
  47. package/dist/evm/client/Charge.d.ts +102 -0
  48. package/dist/evm/client/Charge.d.ts.map +1 -0
  49. package/dist/evm/client/Charge.js +141 -0
  50. package/dist/evm/client/Charge.js.map +1 -0
  51. package/dist/evm/client/Methods.d.ts +81 -0
  52. package/dist/evm/client/Methods.d.ts.map +1 -0
  53. package/dist/evm/client/Methods.js +16 -0
  54. package/dist/evm/client/Methods.js.map +1 -0
  55. package/dist/evm/client/index.d.ts +6 -0
  56. package/dist/evm/client/index.d.ts.map +1 -0
  57. package/dist/evm/client/index.js +6 -0
  58. package/dist/evm/client/index.js.map +1 -0
  59. package/dist/evm/index.d.ts +9 -0
  60. package/dist/evm/index.d.ts.map +1 -0
  61. package/dist/evm/index.js +8 -0
  62. package/dist/evm/index.js.map +1 -0
  63. package/dist/evm/server/Charge.d.ts +62 -0
  64. package/dist/evm/server/Charge.d.ts.map +1 -0
  65. package/dist/evm/server/Charge.js +172 -0
  66. package/dist/evm/server/Charge.js.map +1 -0
  67. package/dist/evm/server/Methods.d.ts +80 -0
  68. package/dist/evm/server/Methods.d.ts.map +1 -0
  69. package/dist/evm/server/Methods.js +16 -0
  70. package/dist/evm/server/Methods.js.map +1 -0
  71. package/dist/evm/server/index.d.ts +6 -0
  72. package/dist/evm/server/index.d.ts.map +1 -0
  73. package/dist/evm/server/index.js +6 -0
  74. package/dist/evm/server/index.js.map +1 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/internal/HeaderCodec.d.ts +18 -0
  80. package/dist/internal/HeaderCodec.d.ts.map +1 -0
  81. package/dist/internal/HeaderCodec.js +31 -0
  82. package/dist/internal/HeaderCodec.js.map +1 -0
  83. package/dist/middlewares/elysia.d.ts.map +1 -1
  84. package/dist/middlewares/elysia.js +2 -3
  85. package/dist/middlewares/elysia.js.map +1 -1
  86. package/dist/middlewares/express.js +2 -1
  87. package/dist/middlewares/express.js.map +1 -1
  88. package/dist/proxy/internal/Headers.d.ts +13 -1
  89. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  90. package/dist/proxy/internal/Headers.js +25 -2
  91. package/dist/proxy/internal/Headers.js.map +1 -1
  92. package/dist/proxy/services/openai.d.ts.map +1 -1
  93. package/dist/proxy/services/openai.js +2 -0
  94. package/dist/proxy/services/openai.js.map +1 -1
  95. package/dist/server/Methods.d.ts +1 -0
  96. package/dist/server/Methods.d.ts.map +1 -1
  97. package/dist/server/Methods.js +1 -0
  98. package/dist/server/Methods.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +90 -12
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Transport.d.ts +10 -0
  103. package/dist/server/Transport.d.ts.map +1 -1
  104. package/dist/server/Transport.js +9 -0
  105. package/dist/server/Transport.js.map +1 -1
  106. package/dist/server/index.d.ts +1 -1
  107. package/dist/server/index.d.ts.map +1 -1
  108. package/dist/server/index.js +1 -1
  109. package/dist/server/index.js.map +1 -1
  110. package/dist/stripe/server/Charge.d.ts +31 -1
  111. package/dist/stripe/server/Charge.d.ts.map +1 -1
  112. package/dist/stripe/server/Charge.js +88 -11
  113. package/dist/stripe/server/Charge.js.map +1 -1
  114. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  115. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  116. package/dist/stripe/server/internal/html.gen.js +1 -1
  117. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  118. package/dist/tempo/client/ChannelOps.d.ts +3 -3
  119. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  120. package/dist/tempo/client/ChannelOps.js +13 -6
  121. package/dist/tempo/client/ChannelOps.js.map +1 -1
  122. package/dist/tempo/client/Charge.d.ts.map +1 -1
  123. package/dist/tempo/client/Charge.js +8 -5
  124. package/dist/tempo/client/Charge.js.map +1 -1
  125. package/dist/tempo/client/Methods.d.ts +0 -1
  126. package/dist/tempo/client/Methods.d.ts.map +1 -1
  127. package/dist/tempo/client/Session.d.ts +2 -4
  128. package/dist/tempo/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/client/Session.js +10 -12
  130. package/dist/tempo/client/Session.js.map +1 -1
  131. package/dist/tempo/client/SessionManager.d.ts +3 -3
  132. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/client/SessionManager.js +1 -1
  134. package/dist/tempo/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/internal/account.d.ts +5 -0
  136. package/dist/tempo/internal/account.d.ts.map +1 -1
  137. package/dist/tempo/internal/account.js +8 -0
  138. package/dist/tempo/internal/account.js.map +1 -1
  139. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  140. package/dist/tempo/internal/fee-payer.js +5 -2
  141. package/dist/tempo/internal/fee-payer.js.map +1 -1
  142. package/dist/tempo/server/Charge.d.ts +6 -0
  143. package/dist/tempo/server/Charge.d.ts.map +1 -1
  144. package/dist/tempo/server/Charge.js +30 -3
  145. package/dist/tempo/server/Charge.js.map +1 -1
  146. package/dist/tempo/server/Session.d.ts +6 -0
  147. package/dist/tempo/server/Session.d.ts.map +1 -1
  148. package/dist/tempo/server/Session.js +14 -13
  149. package/dist/tempo/server/Session.js.map +1 -1
  150. package/dist/tempo/server/Subscription.d.ts +6 -0
  151. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  152. package/dist/tempo/server/Subscription.js +1 -1
  153. package/dist/tempo/server/Subscription.js.map +1 -1
  154. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  155. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  156. package/dist/tempo/server/internal/html.gen.js +1 -1
  157. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  158. package/dist/tempo/session/Chain.d.ts +2 -0
  159. package/dist/tempo/session/Chain.d.ts.map +1 -1
  160. package/dist/tempo/session/Chain.js +8 -8
  161. package/dist/tempo/session/Chain.js.map +1 -1
  162. package/dist/tempo/session/Voucher.d.ts +4 -3
  163. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  164. package/dist/tempo/session/Voucher.js +71 -44
  165. package/dist/tempo/session/Voucher.js.map +1 -1
  166. package/dist/tempo/session/Ws.d.ts.map +1 -1
  167. package/dist/tempo/session/Ws.js +15 -0
  168. package/dist/tempo/session/Ws.js.map +1 -1
  169. package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
  170. package/dist/x402/Assets.d.ts +29 -0
  171. package/dist/x402/Assets.d.ts.map +1 -0
  172. package/dist/x402/Assets.js +46 -0
  173. package/dist/x402/Assets.js.map +1 -0
  174. package/dist/x402/Header.d.ts +14 -0
  175. package/dist/x402/Header.d.ts.map +1 -0
  176. package/dist/x402/Header.js +18 -0
  177. package/dist/x402/Header.js.map +1 -0
  178. package/dist/x402/Types.d.ts +289 -0
  179. package/dist/x402/Types.d.ts.map +1 -0
  180. package/dist/x402/Types.js +139 -0
  181. package/dist/x402/Types.js.map +1 -0
  182. package/dist/x402/client/Exact.d.ts +38 -0
  183. package/dist/x402/client/Exact.d.ts.map +1 -0
  184. package/dist/x402/client/Exact.js +141 -0
  185. package/dist/x402/client/Exact.js.map +1 -0
  186. package/dist/x402/index.d.ts +6 -0
  187. package/dist/x402/index.d.ts.map +1 -0
  188. package/dist/x402/index.js +6 -0
  189. package/dist/x402/index.js.map +1 -0
  190. package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
  191. package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
  192. package/dist/x402/internal/ChallengeBrand.js +13 -0
  193. package/dist/x402/internal/ChallengeBrand.js.map +1 -0
  194. package/dist/x402/internal/RouteBinding.d.ts +8 -0
  195. package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
  196. package/dist/x402/internal/RouteBinding.js +12 -0
  197. package/dist/x402/internal/RouteBinding.js.map +1 -0
  198. package/dist/x402/server/EvmCharge.d.ts +50 -0
  199. package/dist/x402/server/EvmCharge.d.ts.map +1 -0
  200. package/dist/x402/server/EvmCharge.js +301 -0
  201. package/dist/x402/server/EvmCharge.js.map +1 -0
  202. package/dist/x402/server/Facilitator.d.ts +12 -0
  203. package/dist/x402/server/Facilitator.d.ts.map +1 -0
  204. package/dist/x402/server/Facilitator.js +42 -0
  205. package/dist/x402/server/Facilitator.js.map +1 -0
  206. package/package.json +41 -21
  207. package/src/Challenge.test.ts +28 -0
  208. package/src/Challenge.ts +17 -10
  209. package/src/Method.ts +1 -1
  210. package/src/Store.test-d.ts +58 -0
  211. package/src/Store.test.ts +77 -0
  212. package/src/Store.ts +155 -74
  213. package/src/client/Methods.ts +1 -0
  214. package/src/client/Mppx.ts +4 -3
  215. package/src/client/Transport.test.ts +165 -30
  216. package/src/client/Transport.ts +76 -8
  217. package/src/client/index.ts +1 -1
  218. package/src/client/internal/Fetch.test.ts +31 -2
  219. package/src/client/internal/Fetch.ts +26 -19
  220. package/src/evm/Assets.ts +1 -0
  221. package/src/evm/Chains.ts +5 -0
  222. package/src/evm/Methods.ts +44 -0
  223. package/src/evm/PublicInterface.test-d.ts +109 -0
  224. package/src/evm/Types.ts +140 -0
  225. package/src/evm/client/Charge.test.ts +99 -0
  226. package/src/evm/client/Charge.ts +198 -0
  227. package/src/evm/client/Methods.ts +19 -0
  228. package/src/evm/client/index.ts +5 -0
  229. package/src/evm/index.ts +13 -0
  230. package/src/evm/server/Charge.test.ts +199 -0
  231. package/src/evm/server/Charge.ts +283 -0
  232. package/src/evm/server/Methods.ts +22 -0
  233. package/src/evm/server/index.ts +5 -0
  234. package/src/index.ts +2 -0
  235. package/src/internal/HeaderCodec.ts +36 -0
  236. package/src/middlewares/elysia.test.ts +25 -0
  237. package/src/middlewares/elysia.ts +1 -2
  238. package/src/middlewares/express.test.ts +28 -0
  239. package/src/middlewares/express.ts +1 -1
  240. package/src/middlewares/hono.test.ts +138 -2
  241. package/src/middlewares/nextjs.test.ts +22 -0
  242. package/src/proxy/internal/Headers.test.ts +38 -0
  243. package/src/proxy/internal/Headers.ts +26 -2
  244. package/src/proxy/services/openai.test.ts +57 -1
  245. package/src/proxy/services/openai.ts +2 -0
  246. package/src/server/Methods.ts +1 -0
  247. package/src/server/Mppx.test.ts +244 -1
  248. package/src/server/Mppx.ts +124 -11
  249. package/src/server/NodeListener.test.ts +28 -1
  250. package/src/server/Transport.test.ts +19 -0
  251. package/src/server/Transport.ts +20 -0
  252. package/src/server/index.ts +1 -1
  253. package/src/stripe/server/Charge.test.ts +215 -1
  254. package/src/stripe/server/Charge.ts +150 -20
  255. package/src/stripe/server/internal/html.gen.ts +1 -1
  256. package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
  257. package/src/tempo/client/ChannelOps.test.ts +61 -7
  258. package/src/tempo/client/ChannelOps.ts +18 -7
  259. package/src/tempo/client/Charge.test.ts +126 -0
  260. package/src/tempo/client/Charge.ts +10 -6
  261. package/src/tempo/client/Session.test.ts +130 -1
  262. package/src/tempo/client/Session.ts +12 -19
  263. package/src/tempo/client/SessionManager.test.ts +69 -2
  264. package/src/tempo/client/SessionManager.ts +4 -4
  265. package/src/tempo/internal/account.ts +13 -0
  266. package/src/tempo/internal/fee-payer.test.ts +32 -2
  267. package/src/tempo/internal/fee-payer.ts +6 -2
  268. package/src/tempo/server/Charge.test.ts +91 -1
  269. package/src/tempo/server/Charge.ts +48 -2
  270. package/src/tempo/server/Session.test.ts +30 -0
  271. package/src/tempo/server/Session.ts +24 -17
  272. package/src/tempo/server/Subscription.ts +7 -1
  273. package/src/tempo/server/internal/html.gen.ts +1 -1
  274. package/src/tempo/session/Chain.test.ts +4 -4
  275. package/src/tempo/session/Chain.ts +10 -6
  276. package/src/tempo/session/ChannelStore.test.ts +21 -0
  277. package/src/tempo/session/Voucher.test.ts +230 -1
  278. package/src/tempo/session/Voucher.ts +96 -48
  279. package/src/tempo/session/Ws.test.ts +71 -0
  280. package/src/tempo/session/Ws.ts +13 -0
  281. package/src/tempo/subscription/Store.test.ts +55 -0
  282. package/src/x402/Assets.ts +65 -0
  283. package/src/x402/Exact.e2e.test.ts +448 -0
  284. package/src/x402/Header.test.ts +73 -0
  285. package/src/x402/Header.ts +34 -0
  286. package/src/x402/PublicInterface.test-d.ts +39 -0
  287. package/src/x402/Types.ts +248 -0
  288. package/src/x402/client/Exact.test.ts +180 -0
  289. package/src/x402/client/Exact.ts +198 -0
  290. package/src/x402/index.ts +5 -0
  291. package/src/x402/internal/ChallengeBrand.ts +14 -0
  292. package/src/x402/internal/RouteBinding.ts +18 -0
  293. package/src/x402/server/EvmCharge.ts +394 -0
  294. package/src/x402/server/Facilitator.test.ts +111 -0
  295. package/src/x402/server/Facilitator.ts +56 -0
@@ -0,0 +1,39 @@
1
+ import { evm, Mppx } from 'mppx/server'
2
+ import type { Account } from 'viem'
3
+ import { describe, expectTypeOf, test } from 'vp/test'
4
+
5
+ import { evm as clientEvm } from '../client/index.js'
6
+
7
+ const secretKey = 'test-secret'
8
+
9
+ describe('x402 public interface', () => {
10
+ test('server evm charge accepts known assets without transfer metadata', () => {
11
+ const mppx = Mppx.create({
12
+ methods: [
13
+ evm.charge({
14
+ currency: evm.assets.base.USDC,
15
+ recipient: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
16
+ x402: {
17
+ facilitator: 'https://x402.org/facilitator',
18
+ },
19
+ }),
20
+ ],
21
+ secretKey,
22
+ })
23
+
24
+ expectTypeOf(mppx.evm.charge).toBeFunction()
25
+ expectTypeOf(mppx.evm.charge({ amount: '0.01' })).toBeFunction()
26
+ })
27
+
28
+ test('client evm charge exposes account config and policies', () => {
29
+ const method = clientEvm.charge({
30
+ account: {} as Account,
31
+ currencies: [clientEvm.assets.baseSepolia.USDC],
32
+ maxAmount: '0.01',
33
+ networks: [clientEvm.chains.baseSepolia],
34
+ })
35
+
36
+ expectTypeOf(method.intent).toEqualTypeOf<'charge'>()
37
+ expectTypeOf(method.name).toEqualTypeOf<'evm'>()
38
+ })
39
+ })
@@ -0,0 +1,248 @@
1
+ import * as z from '../zod.js'
2
+
3
+ export const versions = [2] as const
4
+
5
+ /** mppx method name used for EVM charge challenges backed by x402. */
6
+ export const paymentMethod = 'evm' as const
7
+
8
+ /** mppx intent name used for EVM charge challenges backed by x402 exact. */
9
+ export const exactIntent = 'charge' as const
10
+
11
+ export const schemes = ['exact'] as const
12
+ export const assetTransferMethods = ['eip3009', 'permit2'] as const
13
+
14
+ /** CAIP-2 namespace prefix for EVM networks. */
15
+ export const evmNetworkPrefix = 'eip155:' as const
16
+
17
+ /** Prefix for synthetic mppx challenge IDs derived from x402 `accepts` entries. */
18
+ export const syntheticChallengeIdPrefix = 'x402:' as const
19
+
20
+ /** x402 protocol version supported by this package. */
21
+ export type Version = 2
22
+
23
+ /** mppx payment method name used for EVM charge challenges backed by x402. */
24
+ export type PaymentMethod = typeof paymentMethod
25
+
26
+ /** mppx intent name used for EVM charge challenges backed by x402 exact. */
27
+ export type ExactIntent = typeof exactIntent
28
+
29
+ /** x402 scheme supported by this package. */
30
+ export type Scheme = (typeof schemes)[number]
31
+
32
+ /** x402 exact EVM asset transfer method. */
33
+ export type AssetTransferMethod = (typeof assetTransferMethods)[number]
34
+
35
+ /** CAIP-2 EVM network identifier. */
36
+ export type EvmNetwork = `${typeof evmNetworkPrefix}${number}`
37
+
38
+ /** HTTP header carrying a base64-encoded x402 payment-required response. */
39
+ export const paymentRequiredHeader = 'PAYMENT-REQUIRED'
40
+
41
+ /** HTTP header carrying a base64-encoded x402 payment payload. */
42
+ export const paymentSignatureHeader = 'PAYMENT-SIGNATURE'
43
+
44
+ /** HTTP header carrying a base64-encoded x402 settlement response. */
45
+ export const paymentResponseHeader = 'PAYMENT-RESPONSE'
46
+
47
+ const nonEmptyString = z.string().check(z.minLength(1))
48
+ const positiveNumber = z.number().check(z.refine((value) => value > 0, 'Must be positive'))
49
+ const atomicAmount = z.string().check(z.regex(/^\d+$/, 'Invalid atomic amount'))
50
+ const address = z.address()
51
+ const evmNetwork = z
52
+ .string()
53
+ .check(
54
+ z.regex(new RegExp(`^${evmNetworkPrefix}\\d+$`), 'Invalid EVM CAIP-2 network'),
55
+ ) as z.ZodMiniType<EvmNetwork>
56
+
57
+ /** Describes the protected resource in x402 v2 payment-required responses. */
58
+ export const ResourceInfoSchema = z.object({
59
+ description: z.optional(z.string()),
60
+ iconUrl: z.optional(z.string()),
61
+ mimeType: z.optional(z.string()),
62
+ serviceName: z.optional(z.string()),
63
+ tags: z.optional(z.array(z.string())),
64
+ url: nonEmptyString,
65
+ })
66
+
67
+ /** Describes the protected resource in x402 v2 payment-required responses. */
68
+ export type ResourceInfo = z.infer<typeof ResourceInfoSchema>
69
+
70
+ /** Public transfer configuration for exact EVM payments. */
71
+ export const ExactTransferSchema = z.discriminatedUnion('type', [
72
+ z.object({
73
+ name: nonEmptyString,
74
+ type: z.literal('eip3009'),
75
+ version: nonEmptyString,
76
+ }),
77
+ z.object({
78
+ name: z.optional(z.string()),
79
+ type: z.literal('permit2'),
80
+ version: z.optional(z.string()),
81
+ }),
82
+ ])
83
+
84
+ /** Public EIP-3009 transfer configuration for exact EVM payments. */
85
+ export type ExactEip3009Transfer = Extract<z.infer<typeof ExactTransferSchema>, { type: 'eip3009' }>
86
+
87
+ /** Public Permit2 transfer configuration for exact EVM payments. */
88
+ export type ExactPermit2Transfer = Extract<z.infer<typeof ExactTransferSchema>, { type: 'permit2' }>
89
+
90
+ /** Public transfer configuration for exact EVM payments. */
91
+ export type ExactTransfer = z.infer<typeof ExactTransferSchema>
92
+
93
+ /** Known asset metadata used to derive x402 wire `extra` fields. */
94
+ export type Asset = {
95
+ address: `0x${string}`
96
+ decimals: number
97
+ transfer: ExactTransfer
98
+ }
99
+
100
+ /** x402 v2 payment requirements for the `exact` scheme. */
101
+ export const PaymentRequirementsSchema = z.object({
102
+ amount: atomicAmount,
103
+ asset: nonEmptyString,
104
+ extra: z.optional(z.record(z.string(), z.unknown())),
105
+ maxTimeoutSeconds: positiveNumber,
106
+ network: evmNetwork,
107
+ payTo: nonEmptyString,
108
+ scheme: z.enum(schemes),
109
+ })
110
+
111
+ /** x402 v2 payment requirements for the `exact` scheme. */
112
+ export type PaymentRequirements = z.infer<typeof PaymentRequirementsSchema>
113
+
114
+ /** x402 v2 protocol extension value. */
115
+ export const ExtensionSchema = z.object({
116
+ info: z.record(z.string(), z.unknown()),
117
+ schema: z.record(z.string(), z.unknown()),
118
+ })
119
+
120
+ /** x402 v2 protocol extension value. */
121
+ export type Extension = z.infer<typeof ExtensionSchema>
122
+
123
+ /** x402 v2 protocol extensions map. */
124
+ export const ExtensionsSchema = z.record(z.string(), ExtensionSchema)
125
+
126
+ /** x402 v2 protocol extensions map. */
127
+ export type Extensions = z.infer<typeof ExtensionsSchema>
128
+
129
+ /** Public exact EVM route request accepted by `mppx` handlers. */
130
+ export type ExactRequest = PaymentRequirements & {
131
+ extensions?: Extensions | undefined
132
+ resource?: ResourceInfo | undefined
133
+ }
134
+
135
+ /** x402 v2 payment-required response. */
136
+ export const PaymentRequiredSchema = z.object({
137
+ accepts: z.array(PaymentRequirementsSchema).check(z.minLength(1)),
138
+ error: z.optional(z.string()),
139
+ extensions: z.optional(ExtensionsSchema),
140
+ resource: ResourceInfoSchema,
141
+ x402Version: z.literal(2),
142
+ })
143
+
144
+ /** x402 v2 payment-required response. */
145
+ export type PaymentRequired = z.infer<typeof PaymentRequiredSchema>
146
+
147
+ /** EIP-3009 transferWithAuthorization payload for exact EVM payments. */
148
+ export const ExactEip3009PayloadSchema = z.object({
149
+ authorization: z.object({
150
+ from: address,
151
+ nonce: z.hash(),
152
+ to: address,
153
+ validAfter: atomicAmount,
154
+ validBefore: atomicAmount,
155
+ value: atomicAmount,
156
+ }),
157
+ signature: z.signature(),
158
+ })
159
+
160
+ /** EIP-3009 transferWithAuthorization payload for exact EVM payments. */
161
+ export type ExactEip3009Payload = z.infer<typeof ExactEip3009PayloadSchema>
162
+
163
+ /** Permit2 payload for exact EVM payments. */
164
+ export const ExactPermit2PayloadSchema = z.object({
165
+ permit2Authorization: z.object({
166
+ deadline: atomicAmount,
167
+ from: address,
168
+ nonce: atomicAmount,
169
+ permitted: z.object({
170
+ amount: atomicAmount,
171
+ token: address,
172
+ }),
173
+ spender: address,
174
+ witness: z.object({
175
+ to: address,
176
+ validAfter: atomicAmount,
177
+ }),
178
+ }),
179
+ signature: z.signature(),
180
+ })
181
+
182
+ /** Permit2 payload for exact EVM payments. */
183
+ export type ExactPermit2Payload = z.infer<typeof ExactPermit2PayloadSchema>
184
+
185
+ /** Exact EVM payment payload body. */
186
+ export const ExactPayloadSchema = z.union([ExactEip3009PayloadSchema, ExactPermit2PayloadSchema])
187
+
188
+ /** Exact EVM payment payload body. */
189
+ export type ExactPayload = z.infer<typeof ExactPayloadSchema>
190
+
191
+ /** x402 v2 payment payload. */
192
+ export const PaymentPayloadSchema = z.object({
193
+ accepted: PaymentRequirementsSchema,
194
+ extensions: z.optional(ExtensionsSchema),
195
+ payload: ExactPayloadSchema,
196
+ resource: z.optional(ResourceInfoSchema),
197
+ x402Version: z.literal(2),
198
+ })
199
+
200
+ /** x402 v2 payment payload. */
201
+ export type PaymentPayload = z.infer<typeof PaymentPayloadSchema>
202
+
203
+ /** Facilitator verification response. */
204
+ export const VerifyResponseSchema = z.object({
205
+ extensions: z.optional(ExtensionsSchema),
206
+ extra: z.optional(z.record(z.string(), z.unknown())),
207
+ invalidMessage: z.optional(z.string()),
208
+ invalidReason: z.optional(z.string()),
209
+ isValid: z.boolean(),
210
+ payer: z.optional(z.string()),
211
+ })
212
+
213
+ /** Facilitator verification response. */
214
+ export type VerifyResponse = z.infer<typeof VerifyResponseSchema>
215
+
216
+ /** Facilitator settlement response and x402 `PAYMENT-RESPONSE` body. */
217
+ export const SettleResponseSchema = z.object({
218
+ amount: z.optional(atomicAmount),
219
+ errorMessage: z.optional(z.string()),
220
+ errorReason: z.optional(z.string()),
221
+ extensions: z.optional(ExtensionsSchema),
222
+ extra: z.optional(z.record(z.string(), z.unknown())),
223
+ network: nonEmptyString,
224
+ payer: z.optional(z.string()),
225
+ success: z.boolean(),
226
+ transaction: z.string(),
227
+ })
228
+
229
+ /** Facilitator settlement response and x402 `PAYMENT-RESPONSE` body. */
230
+ export type SettleResponse = z.infer<typeof SettleResponseSchema>
231
+
232
+ /** x402 facilitator client interface used by server exact config. */
233
+ export type Facilitator = {
234
+ settle: (
235
+ paymentPayload: PaymentPayload,
236
+ paymentRequirements: PaymentRequirements,
237
+ ) => Promise<SettleResponse>
238
+ verify: (
239
+ paymentPayload: PaymentPayload,
240
+ paymentRequirements: PaymentRequirements,
241
+ ) => Promise<VerifyResponse>
242
+ }
243
+
244
+ /** Extracts x402 `PaymentRequirements` from a canonical exact request. */
245
+ export function toPaymentRequirements(request: ExactRequest): PaymentRequirements {
246
+ const { extensions: _extensions, resource: _resource, ...paymentRequirements } = request
247
+ return PaymentRequirementsSchema.parse(paymentRequirements)
248
+ }
@@ -0,0 +1,180 @@
1
+ import { Challenge } from 'mppx'
2
+ import type { Account } from 'viem'
3
+ import { describe, expect, test, vi } from 'vp/test'
4
+
5
+ import * as Chains from '../../evm/Chains.js'
6
+ import * as Assets from '../Assets.js'
7
+ import * as Header from '../Header.js'
8
+ import * as RouteBinding from '../internal/RouteBinding.js'
9
+ import * as Types from '../Types.js'
10
+ import { createCredential } from './Exact.js'
11
+
12
+ type X402Challenge = Parameters<typeof createCredential>[0]['challenge']
13
+
14
+ const account = {
15
+ address: '0x1111111111111111111111111111111111111111',
16
+ signTypedData: vi.fn(async () => '0x1234'),
17
+ } as unknown as Account
18
+ const usdc = Assets.define({
19
+ address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
20
+ decimals: 6,
21
+ network: 'eip155:84532',
22
+ transfer: {
23
+ name: 'USDC',
24
+ type: 'eip3009',
25
+ version: '2',
26
+ },
27
+ })
28
+
29
+ describe('x402 exact credential helper', () => {
30
+ test('enforces max amount, network, and currency policy before signing', async () => {
31
+ const config = {
32
+ account,
33
+ currencies: [usdc],
34
+ maxAmount: '0.01',
35
+ networks: [Chains.baseSepolia],
36
+ } as const
37
+
38
+ await expect(
39
+ createCredential({
40
+ challenge: challenge({ amount: '10001' }),
41
+ config,
42
+ context: {},
43
+ }),
44
+ ).rejects.toThrow('x402 exact amount exceeds maxAmount.')
45
+
46
+ await expect(
47
+ createCredential({
48
+ challenge: challenge({ network: 'eip155:8453' }),
49
+ config,
50
+ context: {},
51
+ }),
52
+ ).rejects.toThrow('x402 exact chain ID is not allowed: 8453.')
53
+
54
+ await expect(
55
+ createCredential({
56
+ challenge: challenge({ asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }),
57
+ config,
58
+ context: {},
59
+ }),
60
+ ).rejects.toThrow(
61
+ 'x402 exact currency is not allowed: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.',
62
+ )
63
+
64
+ expect(account.signTypedData).not.toHaveBeenCalled()
65
+ })
66
+
67
+ test('requires network policy for raw hex currencies', async () => {
68
+ await expect(
69
+ createCredential({
70
+ challenge: challenge(),
71
+ config: {
72
+ account,
73
+ currencies: [usdc.address],
74
+ maxAmount: '0.01',
75
+ },
76
+ context: {},
77
+ }),
78
+ ).rejects.toThrow('x402 exact raw currency allowlists require networks.')
79
+ })
80
+
81
+ test('signs EIP-3009 exact payment payloads', async () => {
82
+ const signTypedData = vi.fn(async () => '0x1234')
83
+ const config = {
84
+ account: {
85
+ ...account,
86
+ signTypedData,
87
+ } as unknown as Account,
88
+ currencies: [usdc],
89
+ maxAmount: '0.01',
90
+ networks: [Chains.baseSepolia],
91
+ } as const
92
+
93
+ const credential = await createCredential({
94
+ challenge: challenge(),
95
+ config,
96
+ context: {},
97
+ })
98
+ const paymentPayload = Header.decodePaymentSignature(credential)
99
+
100
+ expect(signTypedData).toHaveBeenCalledOnce()
101
+ expect(paymentPayload.x402Version).toBe(2)
102
+ expect(paymentPayload.accepted.scheme).toBe('exact')
103
+ expect('authorization' in paymentPayload.payload).toBe(true)
104
+ if (!('authorization' in paymentPayload.payload)) throw new Error()
105
+ expect(paymentPayload.extensions?.mppx?.info.method).toBe('GET')
106
+ expect(paymentPayload.extensions?.mppx?.info.nonce).toMatch(/^[0-9a-f]{64}$/)
107
+ expect(paymentPayload.payload.authorization.nonce).toBe(
108
+ RouteBinding.nonce({
109
+ accepted: paymentPayload.accepted,
110
+ extensions: paymentPayload.extensions!,
111
+ resource: paymentPayload.resource!,
112
+ }),
113
+ )
114
+ expect(paymentPayload.payload.signature).toBe('0x1234')
115
+ })
116
+
117
+ test('uses a fresh route-bound nonce for repeated payments', async () => {
118
+ const config = {
119
+ account,
120
+ currencies: [usdc],
121
+ maxAmount: '0.01',
122
+ networks: [Chains.baseSepolia],
123
+ } as const
124
+
125
+ const first = Header.decodePaymentSignature(
126
+ await createCredential({
127
+ challenge: challenge(),
128
+ config,
129
+ context: {},
130
+ }),
131
+ )
132
+ const second = Header.decodePaymentSignature(
133
+ await createCredential({
134
+ challenge: challenge(),
135
+ config,
136
+ context: {},
137
+ }),
138
+ )
139
+
140
+ expect(first.extensions?.mppx?.info.nonce).not.toBe(second.extensions?.mppx?.info.nonce)
141
+ if (!('authorization' in first.payload) || !('authorization' in second.payload))
142
+ throw new Error()
143
+ expect(first.payload.authorization.nonce).not.toBe(second.payload.authorization.nonce)
144
+ })
145
+ })
146
+
147
+ function challenge(overrides: Partial<Types.PaymentRequirements> = {}): X402Challenge {
148
+ return Challenge.from({
149
+ id: 'x402-test',
150
+ intent: 'charge',
151
+ method: 'evm',
152
+ realm: 'example.com',
153
+ request: {
154
+ amount: '10000',
155
+ asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
156
+ extra: {
157
+ assetTransferMethod: 'eip3009',
158
+ name: 'USDC',
159
+ version: '2',
160
+ },
161
+ maxTimeoutSeconds: 60,
162
+ network: 'eip155:84532',
163
+ payTo: '0x2222222222222222222222222222222222222222',
164
+ scheme: 'exact',
165
+ extensions: {
166
+ mppx: {
167
+ info: { method: 'GET' },
168
+ schema: {
169
+ additionalProperties: false,
170
+ properties: { method: { type: 'string' }, nonce: { type: 'string' } },
171
+ required: ['method'],
172
+ type: 'object',
173
+ },
174
+ },
175
+ },
176
+ resource: { url: 'https://example.com/paid' },
177
+ ...overrides,
178
+ },
179
+ }) as X402Challenge
180
+ }
@@ -0,0 +1,198 @@
1
+ import type { Account } from 'viem'
2
+ import { getAddress, parseUnits } from 'viem'
3
+
4
+ import type * as Challenge from '../../Challenge.js'
5
+ import * as Assets from '../Assets.js'
6
+ import * as Header from '../Header.js'
7
+ import * as RouteBinding from '../internal/RouteBinding.js'
8
+ import * as Types from '../Types.js'
9
+
10
+ /**
11
+ * Creates an x402 exact `PAYMENT-SIGNATURE` credential.
12
+ */
13
+ export async function createCredential(parameters: createCredential.Parameters): Promise<string> {
14
+ const account = (parameters.context?.account ?? parameters.config.account) as Signer
15
+ if (!account.signTypedData) throw new Error('x402 exact requires a typed-data signer.')
16
+
17
+ const request = parameters.challenge.request as Types.ExactRequest
18
+ const accepted = Types.toPaymentRequirements(request)
19
+ assertPolicy(parameters.config, accepted)
20
+ if (!request.resource || !request.extensions?.mppx)
21
+ throw new Error('x402 exact EIP-3009 requires route binding.')
22
+ const extensions = withNonceSalt(request.extensions)
23
+ const transferMethod = accepted.extra?.assetTransferMethod ?? 'eip3009'
24
+ if (transferMethod !== 'eip3009')
25
+ throw new Error(`x402 exact ${String(transferMethod)} signing is not implemented yet.`)
26
+
27
+ const name = accepted.extra?.name
28
+ const version = accepted.extra?.version
29
+ if (typeof name !== 'string' || typeof version !== 'string')
30
+ throw new Error('x402 exact EIP-3009 requires token name and version.')
31
+
32
+ const now = Math.floor(Date.now() / 1000)
33
+ const authorization: Types.ExactEip3009Payload['authorization'] = {
34
+ from: getAddress(account.address),
35
+ nonce: RouteBinding.nonce({
36
+ accepted,
37
+ extensions,
38
+ resource: request.resource,
39
+ }),
40
+ to: getAddress(accepted.payTo),
41
+ validAfter: (now - 600).toString(),
42
+ validBefore: (now + accepted.maxTimeoutSeconds).toString(),
43
+ value: accepted.amount,
44
+ }
45
+ const signature = await account.signTypedData({
46
+ domain: {
47
+ chainId: chainIdOf(accepted.network),
48
+ name,
49
+ verifyingContract: getAddress(accepted.asset),
50
+ version,
51
+ },
52
+ message: {
53
+ ...authorization,
54
+ value: BigInt(authorization.value),
55
+ validAfter: BigInt(authorization.validAfter),
56
+ validBefore: BigInt(authorization.validBefore),
57
+ },
58
+ primaryType: 'TransferWithAuthorization',
59
+ types: authorizationTypes,
60
+ })
61
+
62
+ return Header.encodePaymentSignature({
63
+ accepted,
64
+ extensions,
65
+ payload: {
66
+ authorization,
67
+ signature,
68
+ },
69
+ ...(request.resource ? { resource: request.resource } : {}),
70
+ x402Version: 2,
71
+ })
72
+ }
73
+
74
+ export declare namespace createCredential {
75
+ type Parameters = {
76
+ challenge: Challenge.Challenge<Types.ExactRequest>
77
+ config: Config
78
+ context?: Context | undefined
79
+ }
80
+ }
81
+
82
+ export type Context = {
83
+ account?: Account | undefined
84
+ }
85
+
86
+ export type Signer = Account & {
87
+ signTypedData?: (parameters: any) => Promise<`0x${string}`>
88
+ }
89
+
90
+ export type Config = {
91
+ /** Account used to sign exact EVM payment payloads. */
92
+ account: Account
93
+ /** Optional token decimals used to parse `maxAmount` when currency metadata is not provided. */
94
+ decimals?: number | undefined
95
+ /** Optional maximum display-unit amount the client is willing to pay. */
96
+ maxAmount?: string | undefined
97
+ /** Optional maximum atomic amount the client is willing to pay. */
98
+ maxAtomicAmount?: string | undefined
99
+ /** Optional allowlist of supported EVM chain IDs. */
100
+ networks?: readonly number[] | undefined
101
+ /** Optional allowlist of supported currencies. */
102
+ currencies?: readonly (`0x${string}` | Assets.KnownAsset)[] | undefined
103
+ /** Legacy alias for `currencies`. */
104
+ assets?: readonly (`0x${string}` | Assets.KnownAsset)[] | undefined
105
+ }
106
+
107
+ const authorizationTypes = {
108
+ TransferWithAuthorization: [
109
+ { name: 'from', type: 'address' },
110
+ { name: 'to', type: 'address' },
111
+ { name: 'value', type: 'uint256' },
112
+ { name: 'validAfter', type: 'uint256' },
113
+ { name: 'validBefore', type: 'uint256' },
114
+ { name: 'nonce', type: 'bytes32' },
115
+ ],
116
+ } as const
117
+
118
+ function chainIdOf(network: Types.EvmNetwork): number {
119
+ return Number(network.slice(Types.evmNetworkPrefix.length))
120
+ }
121
+
122
+ function assertPolicy(parameters: Config, accepted: Types.PaymentRequirements) {
123
+ const chainId = chainIdOf(accepted.network)
124
+ if (parameters.networks && !parameters.networks.includes(chainId))
125
+ throw new Error(`x402 exact chain ID is not allowed: ${chainId}.`)
126
+
127
+ const currencies = parameters.currencies ?? parameters.assets
128
+ if (currencies?.some((currency) => !Assets.isAsset(currency)) && !parameters.networks?.length)
129
+ throw new Error('x402 exact raw currency allowlists require networks.')
130
+ if (currencies) {
131
+ const acceptedCurrency = getAddress(accepted.asset as `0x${string}`)
132
+ const allowed = currencies.some((currency) =>
133
+ currencyMatches(currency, acceptedCurrency, accepted.network),
134
+ )
135
+ if (!allowed) throw new Error(`x402 exact currency is not allowed: ${acceptedCurrency}.`)
136
+ }
137
+
138
+ if (
139
+ parameters.maxAtomicAmount !== undefined &&
140
+ BigInt(accepted.amount) > BigInt(parameters.maxAtomicAmount)
141
+ )
142
+ throw new Error('x402 exact amount exceeds maxAtomicAmount.')
143
+
144
+ if (parameters.maxAmount !== undefined) {
145
+ const decimals = decimalsOfAcceptedCurrency(parameters, accepted)
146
+ if (decimals === undefined) throw new Error('x402 exact maxAmount requires currency decimals.')
147
+ if (BigInt(accepted.amount) > parseUnits(parameters.maxAmount, decimals))
148
+ throw new Error('x402 exact amount exceeds maxAmount.')
149
+ }
150
+ }
151
+
152
+ function addressOf(currency: `0x${string}` | Assets.KnownAsset): `0x${string}` {
153
+ return Assets.isAsset(currency) ? currency.address : currency
154
+ }
155
+
156
+ function currencyMatches(
157
+ currency: `0x${string}` | Assets.KnownAsset,
158
+ acceptedCurrency: `0x${string}`,
159
+ network: Types.EvmNetwork,
160
+ ): boolean {
161
+ if (getAddress(addressOf(currency)) !== acceptedCurrency) return false
162
+ return !Assets.isAsset(currency) || currency.network === network
163
+ }
164
+
165
+ function decimalsOfAcceptedCurrency(
166
+ parameters: Config,
167
+ accepted: Types.PaymentRequirements,
168
+ ): number | undefined {
169
+ const currencies = parameters.currencies ?? parameters.assets
170
+ const acceptedCurrency = getAddress(accepted.asset as `0x${string}`)
171
+ const currency = currencies?.find((currency) =>
172
+ currencyMatches(currency, acceptedCurrency, accepted.network),
173
+ )
174
+ if (currency && Assets.isAsset(currency)) return currency.decimals
175
+ return parameters.decimals
176
+ }
177
+
178
+ function withNonceSalt(extensions: Types.Extensions): Types.Extensions {
179
+ const mppx = extensions.mppx
180
+ if (!mppx) return extensions
181
+ return {
182
+ ...extensions,
183
+ mppx: {
184
+ ...mppx,
185
+ info: {
186
+ ...mppx.info,
187
+ nonce: randomNonceSalt(),
188
+ },
189
+ },
190
+ }
191
+ }
192
+
193
+ function randomNonceSalt(): string {
194
+ const crypto = globalThis.crypto
195
+ if (!crypto?.getRandomValues) throw new Error('x402 exact requires crypto randomness.')
196
+ const bytes = crypto.getRandomValues(new Uint8Array(32))
197
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
198
+ }
@@ -0,0 +1,5 @@
1
+ export * as Assets from './Assets.js'
2
+ export * as Header from './Header.js'
3
+ export * as Types from './Types.js'
4
+ export * as assets from './Assets.js'
5
+ export * from './Types.js'
@@ -0,0 +1,14 @@
1
+ const x402Challenge = Symbol.for('mppx.x402.challenge')
2
+
3
+ /** Marks a synthetic challenge as originating from an x402 `PAYMENT-REQUIRED` response. */
4
+ export function mark<const challenge extends object>(challenge: challenge): challenge {
5
+ Object.defineProperty(challenge, x402Challenge, {
6
+ value: true,
7
+ })
8
+ return challenge
9
+ }
10
+
11
+ /** Returns whether a challenge originated from an x402 `PAYMENT-REQUIRED` response. */
12
+ export function is(challenge: unknown): boolean {
13
+ return Boolean((challenge as { [x402Challenge]?: true } | undefined)?.[x402Challenge])
14
+ }