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
@@ -26,6 +26,61 @@ function createRecord(overrides: Partial<SubscriptionRecord> = {}): Subscription
26
26
  }
27
27
 
28
28
  describe('tempo subscription store', () => {
29
+ test('prefixes all backing store keys when configured', async () => {
30
+ const rawStore = Store.memory()
31
+ const store = fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }))
32
+ let finishActivation!: () => void
33
+ const pendingActivation = new Promise<void>((resolve) => {
34
+ finishActivation = resolve
35
+ })
36
+ let activationStarted!: () => void
37
+ const activationStartedPromise = new Promise<void>((resolve) => {
38
+ activationStarted = resolve
39
+ })
40
+
41
+ const activation = store.activate({
42
+ challengeId: 'challenge-1',
43
+ create: async () => {
44
+ activationStarted()
45
+ await pendingActivation
46
+ return { subscription: createRecord() }
47
+ },
48
+ lookupKey: 'user-1:plan:pro',
49
+ })
50
+ await activationStartedPromise
51
+
52
+ expect(await rawStore.get('tenant:tempo:subscription:credential:challenge-1')).not.toBeNull()
53
+ expect(
54
+ await rawStore.get('tenant:tempo:subscription:activation:user-1:plan:pro'),
55
+ ).not.toBeNull()
56
+ expect(await rawStore.get('tempo:subscription:credential:challenge-1')).toBeNull()
57
+
58
+ finishActivation()
59
+ expect((await activation).status).toBe('activated')
60
+ await store.getOrCreateAccessKey('user-1:plan:pro')
61
+
62
+ expect(await rawStore.get(`tenant:tempo:subscription:record:${subscriptionId}`)).not.toBeNull()
63
+ expect(await rawStore.get('tenant:tempo:subscription:key:user-1:plan:pro')).toBe(subscriptionId)
64
+ expect(
65
+ await rawStore.get('tenant:tempo:subscription:access-key:user-1:plan:pro'),
66
+ ).not.toBeNull()
67
+ expect(await rawStore.get(`tempo:subscription:record:${subscriptionId}`)).toBeNull()
68
+ })
69
+
70
+ test('combines store prefix with custom key family prefixes', async () => {
71
+ const rawStore = Store.memory()
72
+ const store = fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }), {
73
+ keyPrefix: 'lookup:',
74
+ recordPrefix: 'record:',
75
+ })
76
+
77
+ await store.put(createRecord())
78
+
79
+ expect(await rawStore.get(`tenant:record:${subscriptionId}`)).not.toBeNull()
80
+ expect(await rawStore.get('tenant:lookup:user-1:plan:pro')).toBe(subscriptionId)
81
+ expect(await rawStore.get(`record:${subscriptionId}`)).toBeNull()
82
+ })
83
+
29
84
  test('rejects a replayed activation challenge', async () => {
30
85
  const store = fromStore(Store.memory())
31
86
 
@@ -0,0 +1,65 @@
1
+ import type { Asset, EvmNetwork, ExactTransfer } from './Types.js'
2
+
3
+ const knownAsset = Symbol('mppx.x402.asset')
4
+
5
+ /** Known x402 asset metadata. */
6
+ export type KnownAsset = Asset & {
7
+ readonly [knownAsset]: true
8
+ network: EvmNetwork
9
+ }
10
+
11
+ /** Creates typed x402 asset metadata for custom tokens. */
12
+ export function define(parameters: define.Parameters): KnownAsset {
13
+ return {
14
+ [knownAsset]: true,
15
+ address: parameters.address,
16
+ decimals: parameters.decimals,
17
+ network: parameters.network,
18
+ transfer: parameters.transfer,
19
+ }
20
+ }
21
+
22
+ export declare namespace define {
23
+ type Parameters = {
24
+ address: `0x${string}`
25
+ decimals: number
26
+ network: EvmNetwork
27
+ transfer: ExactTransfer
28
+ }
29
+ }
30
+
31
+ /** Base network known assets. */
32
+ export const base = {
33
+ USDC: define({
34
+ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
35
+ decimals: 6,
36
+ network: 'eip155:8453',
37
+ transfer: {
38
+ // USDC's EIP-712 domain name differs between Base and Base Sepolia.
39
+ name: 'USD Coin',
40
+ type: 'eip3009',
41
+ version: '2',
42
+ },
43
+ }),
44
+ } as const
45
+
46
+ /** Base Sepolia known assets. */
47
+ export const baseSepolia = {
48
+ USDC: define({
49
+ address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
50
+ decimals: 6,
51
+ network: 'eip155:84532',
52
+ transfer: {
53
+ // Base Sepolia test USDC signs with the shorter EIP-712 domain name.
54
+ name: 'USDC',
55
+ type: 'eip3009',
56
+ version: '2',
57
+ },
58
+ }),
59
+ } as const
60
+
61
+ /** Returns true when a value is known x402 asset metadata. */
62
+ export function isAsset(value: unknown): value is KnownAsset {
63
+ if (typeof value !== 'object' || value === null) return false
64
+ return (value as Partial<KnownAsset>)[knownAsset] === true
65
+ }
@@ -0,0 +1,448 @@
1
+ import { evm as evmClient, Mppx as ClientMppx, tempo as tempoClient } from 'mppx/client'
2
+ import { evm, Mppx as ServerMppx, NodeListener, Request as ServerRequest, tempo } from 'mppx/server'
3
+ import { describe, expect, test } from 'vp/test'
4
+ import * as Http from '~test/Http.js'
5
+ import { accounts, asset, client } from '~test/tempo/viem.js'
6
+
7
+ import * as Header from './Header.js'
8
+ import * as RouteBinding from './internal/RouteBinding.js'
9
+ import * as Types from './Types.js'
10
+
11
+ const secretKey = 'test-secret'
12
+ const transaction = `0x${'1'.repeat(64)}`
13
+
14
+ describe('x402 exact e2e', () => {
15
+ test('serves tempo and x402 paid routes from a live server', async () => {
16
+ const tempoPayment = ServerMppx.create({
17
+ methods: [
18
+ tempo.charge({
19
+ account: accounts[0],
20
+ currency: asset,
21
+ getClient: () => client,
22
+ recipient: accounts[0].address,
23
+ }),
24
+ ],
25
+ secretKey,
26
+ })
27
+
28
+ const facilitator: Types.Facilitator = {
29
+ async verify(paymentPayload) {
30
+ return {
31
+ isValid: true,
32
+ payer: payerOf(paymentPayload),
33
+ }
34
+ },
35
+ async settle(paymentPayload) {
36
+ return {
37
+ network: paymentPayload.accepted.network,
38
+ payer: payerOf(paymentPayload),
39
+ success: true,
40
+ transaction,
41
+ }
42
+ },
43
+ }
44
+ const x402Payment = ServerMppx.create({
45
+ methods: [
46
+ evm.charge({
47
+ currency: evm.assets.baseSepolia.USDC,
48
+ recipient: accounts[0].address,
49
+ x402: { facilitator },
50
+ }),
51
+ ],
52
+ secretKey,
53
+ })
54
+
55
+ const server = await Http.createServer(async (req, res) => {
56
+ const request = ServerRequest.fromNodeListener(req, res)
57
+
58
+ if (req.url === '/tempo') {
59
+ const result = await tempoPayment.tempo.charge({
60
+ amount: '0',
61
+ chainId: client.chain!.id,
62
+ })(request)
63
+ if (result.status === 402) return NodeListener.sendResponse(res, result.challenge)
64
+ return NodeListener.sendResponse(res, result.withReceipt(new Response('tempo ok')))
65
+ }
66
+
67
+ if (req.url === '/x402') {
68
+ const result = await x402Payment.evm.charge({
69
+ amount: '0.01',
70
+ })(request)
71
+ if (result.status === 402) return NodeListener.sendResponse(res, result.challenge)
72
+ return NodeListener.sendResponse(res, result.withReceipt(new Response('x402 ok')))
73
+ }
74
+
75
+ return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
76
+ })
77
+
78
+ try {
79
+ const tempoClientPayment = ClientMppx.create({
80
+ methods: [
81
+ tempoClient.charge({
82
+ account: accounts[0],
83
+ getClient: () => client,
84
+ }),
85
+ ],
86
+ polyfill: false,
87
+ })
88
+ const tempoResponse = await tempoClientPayment.fetch(`${server.url}/tempo`)
89
+ expect(tempoResponse.status).toBe(200)
90
+ expect(await tempoResponse.text()).toBe('tempo ok')
91
+ expect(tempoResponse.headers.has('Payment-Receipt')).toBe(true)
92
+
93
+ const x402ClientPayment = ClientMppx.create({
94
+ methods: [
95
+ evmClient.charge({
96
+ account: accounts[0],
97
+ }),
98
+ ],
99
+ polyfill: false,
100
+ })
101
+ const x402Required = await x402ClientPayment.rawFetch(`${server.url}/x402`)
102
+ expect(x402Required.status).toBe(402)
103
+ expect(x402Required.headers.has(Types.paymentRequiredHeader)).toBe(true)
104
+
105
+ const paymentSignature = await x402ClientPayment.createCredential(
106
+ pureX402Challenge(x402Required),
107
+ )
108
+ const paymentPayload = Header.decodePaymentSignature(paymentSignature)
109
+ expect(paymentPayload.accepted.scheme).toBe('exact')
110
+
111
+ const x402Response = await x402ClientPayment.rawFetch(`${server.url}/x402`, {
112
+ headers: { [Types.paymentSignatureHeader]: paymentSignature },
113
+ })
114
+ expect(x402Response.status).toBe(200)
115
+ expect(await x402Response.text()).toBe('x402 ok')
116
+
117
+ const paymentResponseHeader = x402Response.headers.get(Types.paymentResponseHeader)
118
+ expect(paymentResponseHeader).toBeTruthy()
119
+ expect(Header.decodePaymentResponse(paymentResponseHeader!).transaction).toBe(transaction)
120
+ } finally {
121
+ server.close()
122
+ }
123
+ })
124
+
125
+ test('rejects x402 payment payload replayed across resources with same requirements', async () => {
126
+ let verifyCalls = 0
127
+ const payment = ServerMppx.create({
128
+ methods: [
129
+ evm.charge({
130
+ currency: evm.assets.baseSepolia.USDC,
131
+ recipient: accounts[0].address,
132
+ x402: {
133
+ facilitator: {
134
+ async verify() {
135
+ verifyCalls++
136
+ return { isValid: true }
137
+ },
138
+ async settle(paymentPayload: Types.PaymentPayload) {
139
+ return {
140
+ network: paymentPayload.accepted.network,
141
+ success: true,
142
+ transaction,
143
+ }
144
+ },
145
+ },
146
+ },
147
+ }),
148
+ ],
149
+ secretKey,
150
+ })
151
+ const route = payment.evm.charge({ amount: '0.01' })
152
+
153
+ const routeAChallenge = await route(new Request('https://example.com/a'))
154
+ expect(routeAChallenge.status).toBe(402)
155
+ if (routeAChallenge.status !== 402) throw new Error()
156
+
157
+ const paymentRequired = Header.decodePaymentRequired(
158
+ routeAChallenge.challenge.headers.get(Types.paymentRequiredHeader)!,
159
+ )
160
+ const accepted = paymentRequired.accepts[0]!
161
+ const paymentSignature = Header.encodePaymentSignature({
162
+ accepted,
163
+ payload: {
164
+ authorization: {
165
+ from: accounts[0].address,
166
+ nonce: `0x${'1'.repeat(64)}`,
167
+ to: accepted.payTo as `0x${string}`,
168
+ validAfter: '0',
169
+ validBefore: '9999999999',
170
+ value: accepted.amount,
171
+ },
172
+ signature: `0x${'2'.repeat(130)}`,
173
+ },
174
+ resource: paymentRequired.resource,
175
+ x402Version: 2,
176
+ })
177
+
178
+ const result = await route(
179
+ new Request('https://example.com/b', {
180
+ headers: { [Types.paymentSignatureHeader]: paymentSignature },
181
+ }),
182
+ )
183
+
184
+ expect(result.status).toBe(402)
185
+ expect(verifyCalls).toBe(0)
186
+ })
187
+
188
+ test('rejects x402 route extensions with extra binding fields', async () => {
189
+ let verifyCalls = 0
190
+ const payment = ServerMppx.create({
191
+ methods: [
192
+ evm.charge({
193
+ currency: evm.assets.baseSepolia.USDC,
194
+ recipient: accounts[0].address,
195
+ x402: {
196
+ facilitator: {
197
+ async verify() {
198
+ verifyCalls++
199
+ return { isValid: true }
200
+ },
201
+ async settle(paymentPayload: Types.PaymentPayload) {
202
+ return {
203
+ network: paymentPayload.accepted.network,
204
+ success: true,
205
+ transaction,
206
+ }
207
+ },
208
+ },
209
+ },
210
+ }),
211
+ ],
212
+ secretKey,
213
+ })
214
+ const route = payment.evm.charge({ amount: '0.01' })
215
+
216
+ const first = await route(new Request('https://example.com/a'))
217
+ expect(first.status).toBe(402)
218
+ if (first.status !== 402) throw new Error()
219
+
220
+ const paymentRequired = Header.decodePaymentRequired(
221
+ first.challenge.headers.get(Types.paymentRequiredHeader)!,
222
+ )
223
+ const accepted = paymentRequired.accepts[0]!
224
+ const mppxExtension = paymentRequired.extensions!.mppx
225
+ if (!mppxExtension) throw new Error()
226
+ const extensions: Types.Extensions = {
227
+ ...paymentRequired.extensions!,
228
+ mppx: {
229
+ schema: mppxExtension.schema,
230
+ info: {
231
+ ...mppxExtension.info,
232
+ extra: 'not allowed',
233
+ },
234
+ },
235
+ }
236
+ const paymentSignature = Header.encodePaymentSignature({
237
+ accepted,
238
+ extensions,
239
+ payload: {
240
+ authorization: {
241
+ from: accounts[0].address,
242
+ nonce: RouteBinding.nonce({
243
+ accepted,
244
+ extensions,
245
+ resource: paymentRequired.resource,
246
+ }),
247
+ to: accepted.payTo as `0x${string}`,
248
+ validAfter: '0',
249
+ validBefore: '9999999999',
250
+ value: accepted.amount,
251
+ },
252
+ signature: `0x${'2'.repeat(130)}`,
253
+ },
254
+ resource: paymentRequired.resource,
255
+ x402Version: 2,
256
+ })
257
+
258
+ const result = await route(
259
+ new Request('https://example.com/a', {
260
+ headers: { [Types.paymentSignatureHeader]: paymentSignature },
261
+ }),
262
+ )
263
+
264
+ expect(result.status).toBe(402)
265
+ expect(verifyCalls).toBe(0)
266
+ })
267
+
268
+ test('does not advertise x402 for body-bearing requests without a digest', async () => {
269
+ let verifyCalls = 0
270
+ const payment = ServerMppx.create({
271
+ methods: [
272
+ evm.charge({
273
+ currency: evm.assets.baseSepolia.USDC,
274
+ recipient: accounts[0].address,
275
+ x402: {
276
+ facilitator: {
277
+ async verify() {
278
+ verifyCalls++
279
+ return { isValid: true }
280
+ },
281
+ async settle(paymentPayload: Types.PaymentPayload) {
282
+ return {
283
+ network: paymentPayload.accepted.network,
284
+ success: true,
285
+ transaction,
286
+ }
287
+ },
288
+ },
289
+ },
290
+ }),
291
+ ],
292
+ secretKey,
293
+ })
294
+ const route = payment.evm.charge({ amount: '0.01' })
295
+
296
+ const result = await route(
297
+ new Request('https://example.com/body', {
298
+ body: JSON.stringify({ a: 1 }),
299
+ method: 'POST',
300
+ }),
301
+ )
302
+
303
+ expect(result.status).toBe(402)
304
+ if (result.status !== 402) throw new Error()
305
+ expect(result.challenge.headers.has('WWW-Authenticate')).toBe(true)
306
+ expect(result.challenge.headers.has(Types.paymentRequiredHeader)).toBe(false)
307
+
308
+ const getChallenge = await route(new Request('https://example.com/body'))
309
+ expect(getChallenge.status).toBe(402)
310
+ if (getChallenge.status !== 402) throw new Error()
311
+ const paymentRequired = Header.decodePaymentRequired(
312
+ getChallenge.challenge.headers.get(Types.paymentRequiredHeader)!,
313
+ )
314
+ const accepted = paymentRequired.accepts[0]!
315
+ const paymentSignature = Header.encodePaymentSignature({
316
+ accepted,
317
+ extensions: paymentRequired.extensions,
318
+ payload: {
319
+ authorization: {
320
+ from: accounts[0].address,
321
+ nonce: RouteBinding.nonce({
322
+ accepted,
323
+ extensions: paymentRequired.extensions!,
324
+ resource: paymentRequired.resource,
325
+ }),
326
+ to: accepted.payTo as `0x${string}`,
327
+ validAfter: '0',
328
+ validBefore: '9999999999',
329
+ value: accepted.amount,
330
+ },
331
+ signature: `0x${'2'.repeat(130)}`,
332
+ },
333
+ resource: paymentRequired.resource,
334
+ x402Version: 2,
335
+ })
336
+ const replay = await route(
337
+ new Request('https://example.com/body', {
338
+ body: JSON.stringify({ a: 2 }),
339
+ headers: { [Types.paymentSignatureHeader]: paymentSignature },
340
+ method: 'POST',
341
+ }),
342
+ )
343
+ expect(replay.status).toBe(402)
344
+ expect(verifyCalls).toBe(0)
345
+ })
346
+
347
+ test('serves tempo and x402 from one composed live endpoint', async () => {
348
+ const payment = ServerMppx.create({
349
+ methods: [
350
+ tempo.charge({
351
+ account: accounts[0],
352
+ currency: asset,
353
+ getClient: () => client,
354
+ recipient: accounts[0].address,
355
+ }),
356
+ evm.charge({
357
+ currency: evm.assets.baseSepolia.USDC,
358
+ recipient: accounts[0].address,
359
+ x402: {
360
+ facilitator: {
361
+ async verify(paymentPayload: Types.PaymentPayload) {
362
+ return {
363
+ isValid: true,
364
+ payer: payerOf(paymentPayload),
365
+ }
366
+ },
367
+ async settle(paymentPayload: Types.PaymentPayload) {
368
+ return {
369
+ network: paymentPayload.accepted.network,
370
+ payer: payerOf(paymentPayload),
371
+ success: true,
372
+ transaction,
373
+ }
374
+ },
375
+ },
376
+ },
377
+ }),
378
+ ],
379
+ secretKey,
380
+ })
381
+ const paid = payment.compose(
382
+ [payment.tempo.charge, { amount: '0', chainId: client.chain!.id }],
383
+ [payment.evm.charge, { amount: '0.01' }],
384
+ )
385
+
386
+ const server = await Http.createServer(async (req, res) => {
387
+ const request = ServerRequest.fromNodeListener(req, res)
388
+ const result = await paid(request)
389
+ if (result.status === 402) return NodeListener.sendResponse(res, result.challenge)
390
+ return NodeListener.sendResponse(res, result.withReceipt(new Response('paid ok')))
391
+ })
392
+
393
+ try {
394
+ const challenge = await fetch(server.url)
395
+ expect(challenge.status).toBe(402)
396
+ expect(challenge.headers.has('WWW-Authenticate')).toBe(true)
397
+ expect(challenge.headers.has(Types.paymentRequiredHeader)).toBe(true)
398
+
399
+ const tempoClientPayment = ClientMppx.create({
400
+ methods: [
401
+ tempoClient.charge({
402
+ account: accounts[0],
403
+ getClient: () => client,
404
+ }),
405
+ ],
406
+ polyfill: false,
407
+ })
408
+ const tempoResponse = await tempoClientPayment.fetch(server.url)
409
+ expect(tempoResponse.status).toBe(200)
410
+ expect(await tempoResponse.text()).toBe('paid ok')
411
+ expect(tempoResponse.headers.has('Payment-Receipt')).toBe(true)
412
+
413
+ const x402ClientPayment = ClientMppx.create({
414
+ methods: [
415
+ evmClient.charge({
416
+ account: accounts[0],
417
+ }),
418
+ ],
419
+ polyfill: false,
420
+ })
421
+ const paymentSignature = await x402ClientPayment.createCredential(
422
+ pureX402Challenge(challenge),
423
+ )
424
+ const x402Response = await x402ClientPayment.rawFetch(server.url, {
425
+ headers: { [Types.paymentSignatureHeader]: paymentSignature },
426
+ })
427
+ expect(x402Response.status).toBe(200)
428
+ expect(await x402Response.text()).toBe('paid ok')
429
+ expect(x402Response.headers.has(Types.paymentResponseHeader)).toBe(true)
430
+ } finally {
431
+ server.close()
432
+ }
433
+ })
434
+ })
435
+
436
+ function payerOf(paymentPayload: Types.PaymentPayload): string {
437
+ if ('authorization' in paymentPayload.payload) return paymentPayload.payload.authorization.from
438
+ return paymentPayload.payload.permit2Authorization.from
439
+ }
440
+
441
+ function pureX402Challenge(response: Response): Response {
442
+ const paymentRequired = response.headers.get(Types.paymentRequiredHeader)
443
+ if (!paymentRequired) throw new Error('Missing PAYMENT-REQUIRED header.')
444
+ return new Response(null, {
445
+ headers: { [Types.paymentRequiredHeader]: paymentRequired },
446
+ status: 402,
447
+ })
448
+ }
@@ -0,0 +1,73 @@
1
+ import * as Header from './Header.js'
2
+ import type * as Types from './Types.js'
3
+
4
+ describe('x402 headers', () => {
5
+ test('round trips PAYMENT-REQUIRED header values', () => {
6
+ const paymentRequired: Types.PaymentRequired = {
7
+ accepts: [
8
+ {
9
+ amount: '10000',
10
+ asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
11
+ extra: {
12
+ assetTransferMethod: 'eip3009',
13
+ name: 'USDC',
14
+ version: '2',
15
+ },
16
+ maxTimeoutSeconds: 60,
17
+ network: 'eip155:84532',
18
+ payTo: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
19
+ scheme: 'exact',
20
+ },
21
+ ],
22
+ resource: {
23
+ mimeType: 'application/json',
24
+ url: 'https://api.example.com/premium-data',
25
+ },
26
+ extensions: {
27
+ mppx: {
28
+ info: { method: 'GET' },
29
+ schema: {
30
+ properties: { method: { type: 'string' } },
31
+ required: ['method'],
32
+ type: 'object',
33
+ },
34
+ },
35
+ },
36
+ x402Version: 2,
37
+ }
38
+
39
+ const header = Header.encodePaymentRequired(paymentRequired)
40
+
41
+ expect(Header.decodePaymentRequired(header)).toEqual(paymentRequired)
42
+ })
43
+
44
+ test('round trips PAYMENT-SIGNATURE header values', () => {
45
+ const paymentPayload: Types.PaymentPayload = {
46
+ accepted: {
47
+ amount: '10000',
48
+ asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
49
+ maxTimeoutSeconds: 60,
50
+ network: 'eip155:84532',
51
+ payTo: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
52
+ scheme: 'exact',
53
+ },
54
+ payload: {
55
+ authorization: {
56
+ from: '0x857b06519E91e3A54538791bDbb0E22373e36b66',
57
+ nonce: '0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480',
58
+ to: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
59
+ validAfter: '1740672089',
60
+ validBefore: '1740672154',
61
+ value: '10000',
62
+ },
63
+ signature:
64
+ '0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c',
65
+ },
66
+ x402Version: 2,
67
+ }
68
+
69
+ const header = Header.encodePaymentSignature(paymentPayload)
70
+
71
+ expect(Header.decodePaymentSignature(header)).toEqual(paymentPayload)
72
+ })
73
+ })
@@ -0,0 +1,34 @@
1
+ import * as HeaderCodec from '../internal/HeaderCodec.js'
2
+ import {
3
+ PaymentPayloadSchema,
4
+ PaymentRequiredSchema,
5
+ SettleResponseSchema,
6
+ type PaymentPayload,
7
+ type PaymentRequired,
8
+ type SettleResponse,
9
+ } from './Types.js'
10
+
11
+ const paymentRequired = HeaderCodec.createJson(PaymentRequiredSchema)
12
+ const paymentSignature = HeaderCodec.createJson(PaymentPayloadSchema)
13
+ const paymentResponse = HeaderCodec.createJson(SettleResponseSchema)
14
+
15
+ /** Encodes an x402 payment-required object for the `PAYMENT-REQUIRED` header. */
16
+ export const encodePaymentRequired: (paymentRequired: PaymentRequired) => string =
17
+ paymentRequired.encode
18
+
19
+ /** Decodes an x402 `PAYMENT-REQUIRED` header value. */
20
+ export const decodePaymentRequired: (value: string) => PaymentRequired = paymentRequired.decode
21
+
22
+ /** Encodes an x402 payment payload for the `PAYMENT-SIGNATURE` header. */
23
+ export const encodePaymentSignature: (paymentPayload: PaymentPayload) => string =
24
+ paymentSignature.encode
25
+
26
+ /** Decodes an x402 `PAYMENT-SIGNATURE` header value. */
27
+ export const decodePaymentSignature: (value: string) => PaymentPayload = paymentSignature.decode
28
+
29
+ /** Encodes an x402 settlement response for the `PAYMENT-RESPONSE` header. */
30
+ export const encodePaymentResponse: (paymentResponse: SettleResponse) => string =
31
+ paymentResponse.encode
32
+
33
+ /** Decodes an x402 `PAYMENT-RESPONSE` header value. */
34
+ export const decodePaymentResponse: (value: string) => SettleResponse = paymentResponse.decode