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
@@ -1,9 +1,20 @@
1
1
  import { serve } from '@hono/node-server'
2
2
  import { Hono } from 'hono'
3
3
  import { Challenge, Credential, Method, Receipt, z } from 'mppx'
4
- import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
4
+ import {
5
+ evm as evm_client,
6
+ Mppx as Mppx_client,
7
+ session as sessionIntent,
8
+ tempo as tempo_client,
9
+ } from 'mppx/client'
5
10
  import { Mppx, discovery, payment } from 'mppx/hono'
6
- import { tempo as tempo_server } from 'mppx/server'
11
+ import { evm as evm_server, Mppx as ServerMppx, tempo as tempo_server } from 'mppx/server'
12
+ import {
13
+ paymentRequiredHeader,
14
+ paymentResponseHeader,
15
+ paymentSignatureHeader,
16
+ type PaymentPayload,
17
+ } from 'mppx/x402'
7
18
  import type { Address } from 'viem'
8
19
  import { Addresses } from 'viem/tempo'
9
20
  import { beforeAll, describe, expect, test } from 'vp/test'
@@ -53,6 +64,30 @@ describe('payment', () => {
53
64
 
54
65
  server.close()
55
66
  })
67
+
68
+ test('copies transport-specific success headers', async () => {
69
+ const intent = () => async () => ({
70
+ status: 200 as const,
71
+ withReceipt: (response?: Response) =>
72
+ new Response(response?.body ?? null, {
73
+ headers: {
74
+ ...(response ? Object.fromEntries(response.headers) : {}),
75
+ 'PAYMENT-RESPONSE': 'x402-response',
76
+ },
77
+ status: response?.status ?? 200,
78
+ }),
79
+ })
80
+
81
+ const app = new Hono()
82
+ app.get('/', payment(intent as any, {} as any), (c) => c.json({ data: 'content' }))
83
+
84
+ const server = await createServer(app)
85
+ const response = await globalThis.fetch(server.url)
86
+ expect(response.status).toBe(200)
87
+ expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
88
+
89
+ server.close()
90
+ })
56
91
  })
57
92
 
58
93
  const scopeMethod = Method.toServer(
@@ -187,8 +222,109 @@ describe('charge', () => {
187
222
 
188
223
  server.close()
189
224
  })
225
+
226
+ test('serves tempo and x402 from one Hono endpoint', async () => {
227
+ const transaction = `0x${'2'.repeat(64)}` as const
228
+ const payments = ServerMppx.create({
229
+ methods: [
230
+ tempo_server.charge({
231
+ account: accounts[0],
232
+ currency: asset,
233
+ getClient: () => client,
234
+ recipient: accounts[0].address,
235
+ }),
236
+ evm_server.charge({
237
+ currency: evm_server.assets.baseSepolia.USDC,
238
+ recipient: accounts[0].address,
239
+ x402: {
240
+ facilitator: {
241
+ async verify(paymentPayload: PaymentPayload) {
242
+ return {
243
+ isValid: true,
244
+ payer: payerOf(paymentPayload),
245
+ }
246
+ },
247
+ async settle(paymentPayload: PaymentPayload) {
248
+ return {
249
+ network: paymentPayload.accepted.network,
250
+ payer: payerOf(paymentPayload),
251
+ success: true,
252
+ transaction,
253
+ }
254
+ },
255
+ },
256
+ },
257
+ }),
258
+ ],
259
+ secretKey,
260
+ })
261
+
262
+ const route = payments.compose(
263
+ [payments.tempo.charge, { amount: '0', chainId: client.chain!.id }],
264
+ [payments.evm.charge, { amount: '0.01' }],
265
+ )
266
+
267
+ const app = new Hono()
268
+ app.get('/paid', async (c) => {
269
+ const result = await route(c.req.raw)
270
+ if (result.status === 402) return result.challenge
271
+ return result.withReceipt(c.json({ data: 'paid' }))
272
+ })
273
+
274
+ const server = await createServer(app)
275
+ const challenge = await globalThis.fetch(`${server.url}/paid`)
276
+ expect(challenge.status).toBe(402)
277
+ expect(challenge.headers.get('WWW-Authenticate')).toContain('Payment')
278
+ expect(challenge.headers.get(paymentRequiredHeader)).toBeTruthy()
279
+
280
+ const tempoPayment = Mppx_client.create({
281
+ methods: [
282
+ tempo_client.charge({
283
+ account: accounts[0],
284
+ getClient: () => client,
285
+ }),
286
+ ],
287
+ polyfill: false,
288
+ })
289
+ const tempoResponse = await tempoPayment.fetch(`${server.url}/paid`)
290
+ expect(tempoResponse.status).toBe(200)
291
+ expect(await tempoResponse.json()).toEqual({ data: 'paid' })
292
+ expect(tempoResponse.headers.get('Payment-Receipt')).toBeTruthy()
293
+
294
+ const x402Payment = Mppx_client.create({
295
+ methods: [
296
+ evm_client.charge({
297
+ account: accounts[0],
298
+ }),
299
+ ],
300
+ polyfill: false,
301
+ })
302
+ const paymentSignature = await x402Payment.createCredential(pureX402Challenge(challenge))
303
+ const x402Response = await x402Payment.rawFetch(`${server.url}/paid`, {
304
+ headers: { [paymentSignatureHeader]: paymentSignature },
305
+ })
306
+ expect(x402Response.status).toBe(200)
307
+ expect(await x402Response.json()).toEqual({ data: 'paid' })
308
+ expect(x402Response.headers.get(paymentResponseHeader)).toBeTruthy()
309
+
310
+ server.close()
311
+ })
190
312
  })
191
313
 
314
+ function payerOf(paymentPayload: PaymentPayload): string {
315
+ if ('authorization' in paymentPayload.payload) return paymentPayload.payload.authorization.from
316
+ return paymentPayload.payload.permit2Authorization.from
317
+ }
318
+
319
+ function pureX402Challenge(response: Response): Response {
320
+ const paymentRequired = response.headers.get(paymentRequiredHeader)
321
+ if (!paymentRequired) throw new Error('Missing PAYMENT-REQUIRED header.')
322
+ return new Response(null, {
323
+ headers: { [paymentRequiredHeader]: paymentRequired },
324
+ status: 402,
325
+ })
326
+ }
327
+
192
328
  describe('scope binding', () => {
193
329
  const scopeOpts = {
194
330
  amount: '1',
@@ -59,6 +59,28 @@ describe('payment', () => {
59
59
 
60
60
  server.close()
61
61
  })
62
+
63
+ test('copies transport-specific success headers', async () => {
64
+ const intent = () => async () => ({
65
+ status: 200 as const,
66
+ withReceipt: (response?: Response) =>
67
+ new Response(response?.body ?? null, {
68
+ headers: {
69
+ ...(response ? Object.fromEntries(response.headers) : {}),
70
+ 'PAYMENT-RESPONSE': 'x402-response',
71
+ },
72
+ status: response?.status ?? 200,
73
+ }),
74
+ })
75
+ const handler = payment(intent as any, {} as any, () => Response.json({ data: 'content' }))
76
+
77
+ const server = await createServer(handler)
78
+ const response = await globalThis.fetch(server.url)
79
+ expect(response.status).toBe(200)
80
+ expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
81
+
82
+ server.close()
83
+ })
62
84
  })
63
85
 
64
86
  function createChargeHarness(feePayer: boolean) {
@@ -13,6 +13,26 @@ describe('scrub', () => {
13
13
  expect(result.get('content-type')).toBe('application/json')
14
14
  })
15
15
 
16
+ test('behavior: strips payment protocol headers', () => {
17
+ const headers = new globalThis.Headers({
18
+ 'Accept-Payment': 'evm/charge',
19
+ 'Content-Type': 'application/json',
20
+ 'PAYMENT-RECEIPT': 'receipt',
21
+ 'PAYMENT-REQUIRED': 'required',
22
+ 'PAYMENT-RESPONSE': 'response',
23
+ 'PAYMENT-SIGNATURE': 'signature',
24
+ 'WWW-Authenticate': 'Payment id="abc"',
25
+ })
26
+ const result = Headers.scrub(headers)
27
+ expect(result.has('accept-payment')).toBe(false)
28
+ expect(result.has('payment-receipt')).toBe(false)
29
+ expect(result.has('payment-required')).toBe(false)
30
+ expect(result.has('payment-response')).toBe(false)
31
+ expect(result.has('payment-signature')).toBe(false)
32
+ expect(result.has('www-authenticate')).toBe(false)
33
+ expect(result.get('content-type')).toBe('application/json')
34
+ })
35
+
16
36
  test('behavior: strips cookie header', () => {
17
37
  const headers = new globalThis.Headers({
18
38
  Cookie: 'session=abc123',
@@ -98,4 +118,22 @@ describe('scrubResponse', () => {
98
118
  expect(result.statusText).toBe('Created')
99
119
  expect(await result.text()).toBe('hello')
100
120
  })
121
+
122
+ // Regression: an upstream service must never be able to issue a cookie
123
+ // under the proxy's origin. Otherwise a compromised or attacker-influenced
124
+ // upstream can session-fixate (`Set-Cookie: session=evil; Domain=…`) every
125
+ // sibling subdomain of the proxy. See the docblock on `scrubResponse`.
126
+ test('behavior: strips set-cookie so upstream cannot set cookies on proxy origin', () => {
127
+ const response = new Response('body', {
128
+ headers: [
129
+ ['Set-Cookie', '__Secure-session=evil; Domain=.example.com; Secure; HttpOnly'],
130
+ ['Set-Cookie', 'tracking=1; Path=/'],
131
+ ['Content-Type', 'application/json'],
132
+ ],
133
+ })
134
+ const result = Headers.scrubResponse(response)
135
+ expect(result.headers.has('set-cookie')).toBe(false)
136
+ expect(result.headers.getSetCookie?.() ?? []).toEqual([])
137
+ expect(result.headers.get('content-type')).toBe('application/json')
138
+ })
101
139
  })
@@ -9,6 +9,17 @@ const hopByHopHeaders = new Set([
9
9
  'trailer',
10
10
  ])
11
11
 
12
+ // Payment credentials are consumed by the proxy and must never reach upstream services.
13
+ const paymentHeaders = new Set([
14
+ 'accept-payment',
15
+ 'authorization',
16
+ 'payment-receipt',
17
+ 'payment-required',
18
+ 'payment-response',
19
+ 'payment-signature',
20
+ 'www-authenticate',
21
+ ])
22
+
12
23
  /** Strips hop-by-hop, auth, encoding, cookie, and forwarding headers from a request before proxying upstream. */
13
24
  export function scrub(headers: Headers): Headers {
14
25
  const scrubbed = new Headers()
@@ -16,7 +27,7 @@ export function scrub(headers: Headers): Headers {
16
27
  for (const [name, value] of headers) {
17
28
  const lower = name.toLowerCase()
18
29
 
19
- if (lower === 'authorization') continue
30
+ if (paymentHeaders.has(lower)) continue
20
31
  if (lower === 'accept-encoding') continue
21
32
  if (lower === 'content-length') continue
22
33
  if (lower === 'cookie') continue
@@ -29,11 +40,24 @@ export function scrub(headers: Headers): Headers {
29
40
  return scrubbed
30
41
  }
31
42
 
32
- /** Strips `content-encoding` and `content-length` from an upstream response so the proxy can re-stream it. */
43
+ /**
44
+ * Strips re-streaming headers (`content-encoding`, `content-length`) and
45
+ * security-sensitive headers (`set-cookie`) from an upstream response.
46
+ *
47
+ * `set-cookie` is dropped because a paid API proxy must never let an upstream
48
+ * service set cookies in the user's browser under the proxy's origin. If a
49
+ * compromised, misbehaving, or attacker-influenced upstream returned
50
+ * `Set-Cookie: session=evil; Domain=.example.com`, the browser would honor it
51
+ * for every sibling subdomain of the proxy — turning any future path-confusion
52
+ * or open-redirect bug in the surrounding deployment into a session-fixation
53
+ * primitive. Proxied services authenticate via bearer tokens / signed
54
+ * payloads, never cookies, so dropping `set-cookie` is purely defensive.
55
+ */
33
56
  export function scrubResponse(response: Response): Response {
34
57
  const headers = new Headers(response.headers)
35
58
  headers.delete('content-encoding')
36
59
  headers.delete('content-length')
60
+ headers.delete('set-cookie')
37
61
  return new Response(response.body, {
38
62
  status: response.status,
39
63
  statusText: response.statusText,
@@ -35,8 +35,64 @@ const mppx_client = Mppx_client.create({
35
35
  })
36
36
 
37
37
  let proxyServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
38
+ let upstreamServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
38
39
 
39
- afterEach(() => proxyServer?.close())
40
+ afterEach(() => {
41
+ proxyServer?.close()
42
+ upstreamServer?.close()
43
+ })
44
+
45
+ describe('openai', () => {
46
+ test('security: strips caller-supplied OpenAI tenant headers before proxying', async () => {
47
+ upstreamServer = await Http.createServer((req, res) => {
48
+ res.writeHead(200, { 'Content-Type': 'application/json' })
49
+ res.end(
50
+ JSON.stringify({
51
+ headers: {
52
+ authorization: req.headers.authorization,
53
+ organization: req.headers['openai-organization'],
54
+ project: req.headers['openai-project'],
55
+ },
56
+ }),
57
+ )
58
+ })
59
+
60
+ const proxy = ApiProxy.create({
61
+ services: [
62
+ openai({
63
+ apiKey: 'sk-test',
64
+ baseUrl: upstreamServer.url,
65
+ routes: {
66
+ 'POST /v1/chat/completions': true,
67
+ },
68
+ }),
69
+ ],
70
+ })
71
+ proxyServer = await Http.createServer(proxy.listener)
72
+
73
+ const res = await fetch(`${proxyServer.url}/openai/v1/chat/completions`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'OpenAI-Organization': 'org_attacker',
78
+ 'OpenAI-Project': 'proj_attacker',
79
+ },
80
+ body: '{}',
81
+ })
82
+ expect(res.status).toBe(200)
83
+
84
+ const body = (await res.json()) as {
85
+ headers: {
86
+ authorization: string
87
+ organization?: string | string[] | undefined
88
+ project?: string | string[] | undefined
89
+ }
90
+ }
91
+ expect(body.headers.authorization).toBe('Bearer sk-test')
92
+ expect(body.headers.organization).toBeUndefined()
93
+ expect(body.headers.project).toBeUndefined()
94
+ })
95
+ })
40
96
 
41
97
  describe.skipIf(!apiKey)('openai', () => {
42
98
  test('behavior: proxies GET /v1/models with charge', async () => {
@@ -29,6 +29,8 @@ export function openai(config: openai.Config) {
29
29
  },
30
30
  rewriteRequest(request, ctx) {
31
31
  const apiKey = ctx.apiKey ?? config.apiKey
32
+ request.headers.delete('OpenAI-Organization')
33
+ request.headers.delete('OpenAI-Project')
32
34
  request.headers.set('Authorization', `Bearer ${apiKey}`)
33
35
  return request
34
36
  },
@@ -1,2 +1,3 @@
1
+ export { evm } from '../evm/server/index.js'
1
2
  export { stripe } from '../stripe/server/index.js'
2
3
  export { tempo } from '../tempo/server/index.js'
@@ -6,7 +6,9 @@ import {
6
6
  session as tempo_session_client,
7
7
  tempo as tempo_client,
8
8
  } from 'mppx/client'
9
- import { Mppx, stripe, Store, Transport, tempo } from 'mppx/server'
9
+ import { Types as evm_Types } from 'mppx/evm'
10
+ import { evm, Mppx, stripe, Store, Transport, tempo } from 'mppx/server'
11
+ import { Header as x402_Header, Types as x402_Types, type PaymentPayload } from 'mppx/x402'
10
12
  import { getTransactionReceipt } from 'viem/actions'
11
13
  import { describe, expect, test } from 'vp/test'
12
14
  import * as Http from '~test/Http.js'
@@ -14,6 +16,7 @@ import { deployEscrow } from '~test/tempo/session.js'
14
16
  import { accounts, asset, client } from '~test/tempo/viem.js'
15
17
 
16
18
  import type { SessionReceipt } from '../tempo/session/Types.js'
19
+ import * as x402_RouteBinding from '../x402/internal/RouteBinding.js'
17
20
 
18
21
  const realm = 'api.example.com'
19
22
  const secretKey = 'test-secret-key'
@@ -1855,6 +1858,29 @@ describe('compose', () => {
1855
1858
  },
1856
1859
  })
1857
1860
 
1861
+ const x402Method = evm.charge({
1862
+ currency: evm.assets.baseSepolia.USDC,
1863
+ recipient: accounts[0].address,
1864
+ x402: {
1865
+ facilitator: {
1866
+ async verify(paymentPayload: PaymentPayload) {
1867
+ return {
1868
+ isValid: true,
1869
+ payer: payerOf(paymentPayload),
1870
+ }
1871
+ },
1872
+ async settle(paymentPayload: PaymentPayload) {
1873
+ return {
1874
+ network: paymentPayload.accepted.network,
1875
+ payer: payerOf(paymentPayload),
1876
+ success: true,
1877
+ transaction: `0x${'3'.repeat(64)}`,
1878
+ }
1879
+ },
1880
+ },
1881
+ },
1882
+ })
1883
+
1858
1884
  const challengeOpts = {
1859
1885
  amount: '1000',
1860
1886
  currency: '0x0000000000000000000000000000000000000001',
@@ -1879,6 +1905,142 @@ describe('compose', () => {
1879
1905
  expect(wwwAuth).toContain('method="beta"')
1880
1906
  })
1881
1907
 
1908
+ test('returns composed x402 challenge headers when no credential', async () => {
1909
+ const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
1910
+
1911
+ const result = await mppx.compose(['evm/charge', { amount: '0.01' }])(
1912
+ new Request('https://example.com/resource'),
1913
+ )
1914
+
1915
+ expect(result.status).toBe(402)
1916
+ if (result.status !== 402) throw new Error()
1917
+
1918
+ expect(result.challenge.headers.get('WWW-Authenticate')).toContain('method="evm"')
1919
+ const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
1920
+ expect(header).toBeTruthy()
1921
+ expect(result.challenge.headers.get('Content-Type')).toContain('application/problem+json')
1922
+ expect(await result.challenge.json()).toMatchObject({ status: 402 })
1923
+
1924
+ const paymentRequired = x402_Header.decodePaymentRequired(header!)
1925
+ expect(paymentRequired.accepts).toHaveLength(1)
1926
+ expect(paymentRequired.accepts[0]).toMatchObject({
1927
+ amount: '10000',
1928
+ scheme: x402_Types.schemes[0],
1929
+ })
1930
+ expect(paymentRequired.resource.url).toBe('https://example.com/resource')
1931
+ })
1932
+
1933
+ test('merges Payment auth and x402 challenge headers in compose()', async () => {
1934
+ const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
1935
+
1936
+ const result = await mppx.compose(
1937
+ [alphaMethod, challengeOpts],
1938
+ ['evm/charge', { amount: '0.01' }],
1939
+ )(new Request('https://example.com/resource'))
1940
+
1941
+ expect(result.status).toBe(402)
1942
+ if (result.status !== 402) throw new Error()
1943
+
1944
+ const wwwAuth = result.challenge.headers.get('WWW-Authenticate')
1945
+ expect(wwwAuth).toContain('method="alpha"')
1946
+
1947
+ const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
1948
+ expect(header).toBeTruthy()
1949
+ const paymentRequired = x402_Header.decodePaymentRequired(header!)
1950
+ expect(paymentRequired.accepts.map((accepted) => accepted.amount)).toEqual(['10000'])
1951
+ })
1952
+
1953
+ test('keeps pure x402 challenge headers when Payment auth challenges are present', async () => {
1954
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
1955
+ const pureX402 = async () =>
1956
+ ({
1957
+ status: 402,
1958
+ challenge: new Response('{}', {
1959
+ status: 402,
1960
+ headers: {
1961
+ [x402_Types.paymentRequiredHeader]: x402_Header.encodePaymentRequired({
1962
+ accepts: [
1963
+ {
1964
+ amount: '10000',
1965
+ asset: evm.assets.baseSepolia.USDC.address,
1966
+ extra: {
1967
+ assetTransferMethod: evm_Types.eip3009,
1968
+ name: 'USDC',
1969
+ version: '2',
1970
+ },
1971
+ maxTimeoutSeconds: 300,
1972
+ network: 'eip155:84532',
1973
+ payTo: accounts[0].address,
1974
+ scheme: 'exact',
1975
+ },
1976
+ ],
1977
+ resource: { url: 'https://example.com/resource' },
1978
+ x402Version: 2,
1979
+ }),
1980
+ },
1981
+ }),
1982
+ }) as const
1983
+
1984
+ const alpha = (mppx as unknown as Record<string, (options: unknown) => any>)['alpha/charge']!(
1985
+ challengeOpts,
1986
+ )
1987
+
1988
+ const result = await Mppx.compose(alpha, pureX402)(new Request('https://example.com/resource'))
1989
+
1990
+ expect(result.status).toBe(402)
1991
+ if (result.status !== 402) throw new Error()
1992
+
1993
+ expect(result.challenge.headers.get('WWW-Authenticate')).toContain('method="alpha"')
1994
+ expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeTruthy()
1995
+ })
1996
+
1997
+ test('merges multiple x402 exact offers in compose()', async () => {
1998
+ const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
1999
+
2000
+ const result = await mppx.compose(
2001
+ ['evm/charge', { amount: '0.01' }],
2002
+ ['evm/charge', { amount: '0.02' }],
2003
+ )(new Request('https://example.com/resource'))
2004
+
2005
+ expect(result.status).toBe(402)
2006
+ if (result.status !== 402) throw new Error()
2007
+
2008
+ const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
2009
+ expect(header).toBeTruthy()
2010
+ const paymentRequired = x402_Header.decodePaymentRequired(header!)
2011
+ expect(paymentRequired.accepts.map((accepted) => accepted.amount)).toEqual(['10000', '20000'])
2012
+ })
2013
+
2014
+ test('dispatches x402 credentials through compose()', async () => {
2015
+ const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
2016
+ const handle = mppx.compose([alphaMethod, challengeOpts], ['evm/charge', { amount: '0.01' }])
2017
+
2018
+ const firstResult = await handle(new Request('https://example.com/resource'))
2019
+ expect(firstResult.status).toBe(402)
2020
+ if (firstResult.status !== 402) throw new Error()
2021
+
2022
+ const paymentRequired = x402_Header.decodePaymentRequired(
2023
+ firstResult.challenge.headers.get(x402_Types.paymentRequiredHeader)!,
2024
+ )
2025
+ const credential = await x402PaymentSignature(
2026
+ paymentRequired.accepts[0]!,
2027
+ paymentRequired.resource,
2028
+ paymentRequired.extensions,
2029
+ )
2030
+
2031
+ const result = await handle(
2032
+ new Request('https://example.com/resource', {
2033
+ headers: { [x402_Types.paymentSignatureHeader]: credential },
2034
+ }),
2035
+ )
2036
+
2037
+ expect(result.status).toBe(200)
2038
+ if (result.status !== 200) throw new Error()
2039
+ const response = result.withReceipt(new Response('paid'))
2040
+ expect(response.headers.get(x402_Types.paymentResponseHeader)).toBeTruthy()
2041
+ expect(await response.text()).toBe('paid')
2042
+ })
2043
+
1882
2044
  test('filters compose challenges using Accept-Payment', async () => {
1883
2045
  const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
1884
2046
 
@@ -1899,6 +2061,27 @@ describe('compose', () => {
1899
2061
  expect(challenges[0]?.method).toBe('beta')
1900
2062
  })
1901
2063
 
2064
+ test('filters compose x402 challenge headers using Accept-Payment', async () => {
2065
+ const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
2066
+
2067
+ const result = await mppx.compose(
2068
+ [alphaMethod, challengeOpts],
2069
+ ['evm/charge', { amount: '0.01' }],
2070
+ )(
2071
+ new Request('https://example.com/resource', {
2072
+ headers: { 'Accept-Payment': 'alpha/charge' },
2073
+ }),
2074
+ )
2075
+
2076
+ expect(result.status).toBe(402)
2077
+ if (result.status !== 402) throw new Error()
2078
+
2079
+ const challenges = Challenge.fromResponseList(result.challenge)
2080
+ expect(challenges).toHaveLength(1)
2081
+ expect(challenges[0]?.method).toBe('alpha')
2082
+ expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeNull()
2083
+ })
2084
+
1902
2085
  test('orders compose challenges by Accept-Payment q-value', async () => {
1903
2086
  const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
1904
2087
 
@@ -2522,6 +2705,66 @@ describe('compose', () => {
2522
2705
  })
2523
2706
  })
2524
2707
 
2708
+ async function x402PaymentSignature(
2709
+ accepted: x402_Types.PaymentRequirements,
2710
+ resource: x402_Types.ResourceInfo,
2711
+ extensions: x402_Types.Extensions | undefined,
2712
+ ): Promise<string> {
2713
+ const authorization = {
2714
+ from: accounts[0].address,
2715
+ nonce: x402_RouteBinding.nonce({
2716
+ accepted,
2717
+ extensions: extensions!,
2718
+ resource,
2719
+ }),
2720
+ to: accepted.payTo as `0x${string}`,
2721
+ validAfter: '0',
2722
+ validBefore: '9999999999',
2723
+ value: accepted.amount,
2724
+ }
2725
+ const signature = await accounts[0].signTypedData({
2726
+ domain: {
2727
+ chainId: Number(accepted.network.slice(x402_Types.evmNetworkPrefix.length)),
2728
+ name: accepted.extra?.name as string,
2729
+ verifyingContract: accepted.asset as `0x${string}`,
2730
+ version: accepted.extra?.version as string,
2731
+ },
2732
+ message: {
2733
+ ...authorization,
2734
+ validAfter: BigInt(authorization.validAfter),
2735
+ validBefore: BigInt(authorization.validBefore),
2736
+ value: BigInt(authorization.value),
2737
+ },
2738
+ primaryType: 'TransferWithAuthorization',
2739
+ types: {
2740
+ TransferWithAuthorization: [
2741
+ { name: 'from', type: 'address' },
2742
+ { name: 'to', type: 'address' },
2743
+ { name: 'value', type: 'uint256' },
2744
+ { name: 'validAfter', type: 'uint256' },
2745
+ { name: 'validBefore', type: 'uint256' },
2746
+ { name: 'nonce', type: 'bytes32' },
2747
+ ],
2748
+ },
2749
+ })
2750
+
2751
+ return x402_Header.encodePaymentSignature({
2752
+ accepted,
2753
+ ...(extensions ? { extensions } : {}),
2754
+ payload: {
2755
+ authorization,
2756
+ signature,
2757
+ },
2758
+ resource,
2759
+ x402Version: 2,
2760
+ })
2761
+ }
2762
+
2763
+ function payerOf(paymentPayload: PaymentPayload): string {
2764
+ if ('authorization' in paymentPayload.payload) return paymentPayload.payload.authorization.from
2765
+ return paymentPayload.payload.permit2Authorization.from
2766
+ }
2767
+
2525
2768
  describe('compose: pre-dispatch narrowing edge cases', () => {
2526
2769
  const mockCharge = Method.from({
2527
2770
  name: 'alpha',