mppx 0.7.0 → 0.8.0

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 (278) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -11
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +18 -6
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Mcp.d.ts +3 -0
  7. package/dist/Mcp.d.ts.map +1 -1
  8. package/dist/Mcp.js +2 -0
  9. package/dist/Mcp.js.map +1 -1
  10. package/dist/PaymentRequest.d.ts +10 -10
  11. package/dist/PaymentRequest.js +8 -8
  12. package/dist/client/Mppx.js +2 -2
  13. package/dist/client/Mppx.js.map +1 -1
  14. package/dist/client/Transport.d.ts +11 -16
  15. package/dist/client/Transport.d.ts.map +1 -1
  16. package/dist/client/Transport.js +55 -75
  17. package/dist/client/Transport.js.map +1 -1
  18. package/dist/client/index.d.ts +3 -0
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +1 -0
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/internal/Fetch.d.ts.map +1 -1
  23. package/dist/client/internal/Fetch.js +46 -7
  24. package/dist/client/internal/Fetch.js.map +1 -1
  25. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  26. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  27. package/dist/client/internal/protocols/Mcp.js +159 -0
  28. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  29. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  30. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mpp.js +18 -0
  32. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  33. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  34. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Protocol.js +2 -0
  36. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  37. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  38. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Shared.js +20 -0
  40. package/dist/client/internal/protocols/Shared.js.map +1 -0
  41. package/dist/client/internal/protocols/X402.d.ts +8 -0
  42. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/X402.js +39 -0
  44. package/dist/client/internal/protocols/X402.js.map +1 -0
  45. package/dist/evm/client/index.d.ts +1 -0
  46. package/dist/evm/client/index.d.ts.map +1 -1
  47. package/dist/evm/client/index.js +1 -0
  48. package/dist/evm/client/index.js.map +1 -1
  49. package/dist/evm/index.d.ts +2 -0
  50. package/dist/evm/index.d.ts.map +1 -1
  51. package/dist/evm/index.js +2 -0
  52. package/dist/evm/index.js.map +1 -1
  53. package/dist/evm/server/index.d.ts +1 -0
  54. package/dist/evm/server/index.d.ts.map +1 -1
  55. package/dist/evm/server/index.js +1 -0
  56. package/dist/evm/server/index.js.map +1 -1
  57. package/dist/mcp/client/McpClient.d.ts +101 -0
  58. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  59. package/dist/mcp/client/McpClient.js +162 -0
  60. package/dist/mcp/client/McpClient.js.map +1 -0
  61. package/dist/mcp/client/index.d.ts.map +1 -0
  62. package/dist/mcp/client/index.js.map +1 -0
  63. package/dist/mcp/server/Transport.d.ts.map +1 -0
  64. package/dist/mcp/server/Transport.js.map +1 -0
  65. package/dist/mcp/server/index.d.ts.map +1 -0
  66. package/dist/mcp/server/index.js.map +1 -0
  67. package/dist/server/Mppx.d.ts +1 -1
  68. package/dist/server/Mppx.d.ts.map +1 -1
  69. package/dist/server/Mppx.js +9 -0
  70. package/dist/server/Mppx.js.map +1 -1
  71. package/dist/server/Transport.d.ts +1 -1
  72. package/dist/server/Transport.d.ts.map +1 -1
  73. package/dist/server/Transport.js +1 -1
  74. package/dist/server/Transport.js.map +1 -1
  75. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  76. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  77. package/dist/stripe/server/internal/html.gen.js +1 -1
  78. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  79. package/dist/tempo/Proof.d.ts +85 -1
  80. package/dist/tempo/Proof.d.ts.map +1 -1
  81. package/dist/tempo/Proof.js +35 -0
  82. package/dist/tempo/Proof.js.map +1 -1
  83. package/dist/tempo/client/Charge.d.ts +13 -1
  84. package/dist/tempo/client/Charge.d.ts.map +1 -1
  85. package/dist/tempo/client/Charge.js +38 -25
  86. package/dist/tempo/client/Charge.js.map +1 -1
  87. package/dist/tempo/client/Methods.d.ts +5 -3
  88. package/dist/tempo/client/Methods.d.ts.map +1 -1
  89. package/dist/tempo/client/Methods.js +4 -2
  90. package/dist/tempo/client/Methods.js.map +1 -1
  91. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  92. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  93. package/dist/tempo/client/ResolveAccount.js +2 -0
  94. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  95. package/dist/tempo/internal/fee-payer.d.ts +9 -1
  96. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  97. package/dist/tempo/internal/fee-payer.js +35 -6
  98. package/dist/tempo/internal/fee-payer.js.map +1 -1
  99. package/dist/tempo/internal/proof.d.ts +71 -5
  100. package/dist/tempo/internal/proof.d.ts.map +1 -1
  101. package/dist/tempo/internal/proof.js +42 -6
  102. package/dist/tempo/internal/proof.js.map +1 -1
  103. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  104. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  105. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  106. package/dist/tempo/server/Charge.d.ts.map +1 -1
  107. package/dist/tempo/server/Charge.js +42 -18
  108. package/dist/tempo/server/Charge.js.map +1 -1
  109. package/dist/tempo/server/Methods.d.ts +4 -2
  110. package/dist/tempo/server/Methods.d.ts.map +1 -1
  111. package/dist/tempo/server/Methods.js +4 -2
  112. package/dist/tempo/server/Methods.js.map +1 -1
  113. package/dist/tempo/server/Subscription.d.ts +10 -0
  114. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  115. package/dist/tempo/server/Subscription.js +135 -23
  116. package/dist/tempo/server/Subscription.js.map +1 -1
  117. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  118. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  119. package/dist/tempo/server/internal/html.gen.js +1 -1
  120. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  121. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  122. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  123. package/dist/tempo/session/client/ChannelOps.js +7 -10
  124. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  126. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  127. package/dist/tempo/session/client/ChannelStore.js +63 -0
  128. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  129. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  130. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  131. package/dist/tempo/session/client/CredentialState.js +51 -49
  132. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  133. package/dist/tempo/session/client/Session.d.ts +8 -2
  134. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  135. package/dist/tempo/session/client/Session.js +22 -8
  136. package/dist/tempo/session/client/Session.js.map +1 -1
  137. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  138. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  139. package/dist/tempo/session/client/SessionManager.js +124 -174
  140. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  141. package/dist/tempo/session/client/index.d.ts +3 -4
  142. package/dist/tempo/session/client/index.d.ts.map +1 -1
  143. package/dist/tempo/session/client/index.js +1 -0
  144. package/dist/tempo/session/client/index.js.map +1 -1
  145. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  146. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  147. package/dist/tempo/session/precompile/Voucher.js +24 -25
  148. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  149. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  150. package/dist/tempo/session/server/Settlement.js +4 -2
  151. package/dist/tempo/session/server/Settlement.js.map +1 -1
  152. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  153. package/dist/tempo/session/server/Sse.js.map +1 -1
  154. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  155. package/dist/tempo/session/server/Ws.js.map +1 -1
  156. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  157. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  158. package/dist/tempo/subscription/Store.d.ts +2 -0
  159. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  160. package/dist/tempo/subscription/Store.js +16 -1
  161. package/dist/tempo/subscription/Store.js.map +1 -1
  162. package/dist/x402/index.d.ts +1 -0
  163. package/dist/x402/index.d.ts.map +1 -1
  164. package/dist/x402/index.js +1 -0
  165. package/dist/x402/index.js.map +1 -1
  166. package/package.json +21 -10
  167. package/src/Challenge.test.ts +40 -0
  168. package/src/Challenge.ts +19 -6
  169. package/src/Mcp.ts +4 -0
  170. package/src/PaymentRequest.ts +10 -10
  171. package/src/cli/cli.test.ts +15 -15
  172. package/src/client/Mppx.test-d.ts +21 -1
  173. package/src/client/Mppx.test.ts +1 -1
  174. package/src/client/Mppx.ts +2 -2
  175. package/src/client/Transport.test.ts +225 -178
  176. package/src/client/Transport.ts +77 -83
  177. package/src/client/index.ts +14 -0
  178. package/src/client/internal/Fetch.test.ts +207 -2
  179. package/src/client/internal/Fetch.ts +52 -6
  180. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  181. package/src/client/internal/protocols/Mcp.ts +162 -0
  182. package/src/client/internal/protocols/Mpp.ts +21 -0
  183. package/src/client/internal/protocols/Protocol.ts +10 -0
  184. package/src/client/internal/protocols/Shared.ts +25 -0
  185. package/src/client/internal/protocols/X402.ts +42 -0
  186. package/src/discovery/OpenApi.test.ts +1 -1
  187. package/src/evm/PublicInterface.test-d.ts +1 -1
  188. package/src/evm/client/index.ts +1 -0
  189. package/src/evm/index.ts +2 -0
  190. package/src/evm/server/Charge.test.ts +1 -1
  191. package/src/evm/server/index.ts +1 -0
  192. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  193. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  194. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  195. package/src/mcp/client/McpClient.ts +307 -0
  196. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  197. package/src/middlewares/elysia.test.ts +1 -1
  198. package/src/middlewares/express.test.ts +1 -1
  199. package/src/middlewares/hono.test.ts +1 -1
  200. package/src/middlewares/internal/mppx.test.ts +1 -1
  201. package/src/middlewares/nextjs.test.ts +1 -1
  202. package/src/proxy/Proxy.test.ts +1 -1
  203. package/src/proxy/services/anthropic.test.ts +1 -1
  204. package/src/proxy/services/openai.test.ts +1 -1
  205. package/src/proxy/services/stripe.test.ts +1 -1
  206. package/src/server/Mppx.authorize.test.ts +1 -1
  207. package/src/server/Mppx.test-d.ts +1 -1
  208. package/src/server/Mppx.test.ts +20 -2
  209. package/src/server/Mppx.ts +14 -1
  210. package/src/server/Transport.test.ts +6 -6
  211. package/src/server/Transport.ts +1 -1
  212. package/src/stripe/Charge.integration.test.ts +1 -1
  213. package/src/stripe/client/Charge.test.ts +1 -1
  214. package/src/stripe/server/Charge.test.ts +1 -1
  215. package/src/stripe/server/internal/html/package.json +1 -1
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/Proof.conformance.test.ts +146 -0
  218. package/src/tempo/Proof.test-d.ts +15 -0
  219. package/src/tempo/Proof.ts +52 -1
  220. package/src/tempo/Subscription.integration.test.ts +1 -1
  221. package/src/tempo/client/Charge.test.ts +173 -0
  222. package/src/tempo/client/Charge.ts +65 -36
  223. package/src/tempo/client/Methods.ts +4 -2
  224. package/src/tempo/client/ResolveAccount.ts +46 -0
  225. package/src/tempo/internal/fee-payer.test.ts +65 -10
  226. package/src/tempo/internal/fee-payer.ts +42 -6
  227. package/src/tempo/internal/proof.test.ts +12 -4
  228. package/src/tempo/internal/proof.ts +55 -6
  229. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  230. package/src/tempo/legacy/server/Session.test.ts +91 -26
  231. package/src/tempo/server/Charge.test.ts +388 -17
  232. package/src/tempo/server/Charge.ts +46 -24
  233. package/src/tempo/server/Methods.ts +4 -2
  234. package/src/tempo/server/Subscription.test.ts +465 -3
  235. package/src/tempo/server/Subscription.ts +174 -19
  236. package/src/tempo/server/internal/html/package.json +2 -2
  237. package/src/tempo/server/internal/html.gen.ts +1 -1
  238. package/src/tempo/session/client/ChannelOps.ts +5 -19
  239. package/src/tempo/session/client/ChannelStore.ts +111 -0
  240. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  241. package/src/tempo/session/client/CredentialState.ts +58 -73
  242. package/src/tempo/session/client/Session.test.ts +41 -1
  243. package/src/tempo/session/client/Session.ts +36 -10
  244. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  245. package/src/tempo/session/client/SessionManager.ts +141 -235
  246. package/src/tempo/session/client/index.ts +8 -5
  247. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  248. package/src/tempo/session/precompile/Voucher.ts +27 -25
  249. package/src/tempo/session/server/Session.test.ts +4 -4
  250. package/src/tempo/session/server/Settlement.test.ts +88 -1
  251. package/src/tempo/session/server/Settlement.ts +2 -1
  252. package/src/tempo/session/server/Sse.ts +0 -2
  253. package/src/tempo/session/server/Ws.ts +0 -4
  254. package/src/tempo/subscription/Store.ts +27 -9
  255. package/src/x402/Exact.e2e.test.ts +1 -1
  256. package/src/x402/PublicInterface.test-d.ts +1 -1
  257. package/src/x402/index.ts +1 -0
  258. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  259. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  260. package/dist/mcp-sdk/client/McpClient.js +0 -118
  261. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  262. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  263. package/dist/mcp-sdk/client/index.js.map +0 -1
  264. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  265. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  266. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  267. package/dist/mcp-sdk/server/index.js.map +0 -1
  268. package/src/mcp-sdk/client/McpClient.ts +0 -228
  269. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  270. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  271. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  272. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  273. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  274. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  275. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  276. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  277. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  278. /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
@@ -4,6 +4,8 @@ import { Methods } from 'mppx/tempo'
4
4
  import { Header as x402_Header, Types as x402_Types, type PaymentRequired } from 'mppx/x402'
5
5
  import { describe, expect, test } from 'vp/test'
6
6
 
7
+ import * as x402_ChallengeBrand from '../x402/internal/ChallengeBrand.js'
8
+
7
9
  const realm = 'api.example.com'
8
10
  const secretKey = 'test-secret-key'
9
11
 
@@ -138,13 +140,13 @@ describe('http', () => {
138
140
  }),
139
141
  name: 'Payment auth and x402 challenges when both are present',
140
142
  },
141
- ])('returns $name', ({ expectedIds, expectedMethods, headers }) => {
143
+ ])('returns $name', async ({ expectedIds, expectedMethods, headers }) => {
142
144
  const transport = Transport.http()
143
145
  const response = new Response(null, {
144
146
  status: 402,
145
147
  headers: headers(),
146
148
  })
147
- const challenges = transport.getChallenges?.(response) ?? []
149
+ const challenges = (await transport.getChallenges?.(response)) ?? []
148
150
 
149
151
  expect(challenges.map((entry) => entry.id)).toEqual(expectedIds)
150
152
  expect(challenges.map((entry) => entry.method)).toEqual(expectedMethods)
@@ -160,20 +162,6 @@ describe('http', () => {
160
162
  expectedValue: Credential.serialize(credential),
161
163
  name: 'Payment auth credential for Payment auth challenge',
162
164
  },
163
- {
164
- challenge: Transport.http().getChallenges!(
165
- new Response(null, {
166
- status: 402,
167
- headers: {
168
- 'PAYMENT-REQUIRED': x402_Header.encodePaymentRequired(x402PaymentRequired),
169
- },
170
- }),
171
- )[0],
172
- credential: 'x402-signature',
173
- expectedHeader: 'PAYMENT-SIGNATURE',
174
- expectedValue: 'x402-signature',
175
- name: 'raw x402 credential for x402 challenge',
176
- },
177
165
  {
178
166
  challenge,
179
167
  credential: 'custom-credential',
@@ -197,6 +185,24 @@ describe('http', () => {
197
185
  expect(headers.get(expectedHeader)).toBe(expectedValue)
198
186
  })
199
187
 
188
+ test('writes x402 credentials for x402 challenges from the same transport', () => {
189
+ const transport = Transport.http()
190
+ const [x402Challenge] = transport.getChallenges!(
191
+ new Response(null, {
192
+ status: 402,
193
+ headers: {
194
+ 'PAYMENT-REQUIRED': x402_Header.encodePaymentRequired(x402PaymentRequired),
195
+ },
196
+ }),
197
+ ) as Challenge.Challenge[]
198
+
199
+ const result = transport.setCredential({}, 'x402-signature', { challenge: x402Challenge })
200
+ const headers = result.headers as Headers
201
+
202
+ expect(headers.get('Authorization')).toBeNull()
203
+ expect(headers.get(x402_Types.paymentSignatureHeader)).toBe('x402-signature')
204
+ })
205
+
200
206
  test('does not treat unbranded Payment-auth challenges as x402', () => {
201
207
  const transport = Transport.http()
202
208
  const untrustedChallenge = Challenge.from({
@@ -216,16 +222,53 @@ describe('http', () => {
216
222
  expect(headers.get(x402_Types.paymentSignatureHeader)).toBeNull()
217
223
  })
218
224
 
219
- test('removes stale credential headers before setting the retry credential', () => {
225
+ test('routes JSON-RPC requests with native 402 challenges through Authorization', () => {
226
+ const transport = Transport.http()
227
+ const jsonRpcRequest = {
228
+ method: 'POST',
229
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} }),
230
+ } satisfies RequestInit
231
+ const [nativeChallenge] = transport.getChallenges!(
232
+ new Response(null, {
233
+ status: 402,
234
+ headers: { 'WWW-Authenticate': Challenge.serialize(challenge) },
235
+ }),
236
+ jsonRpcRequest,
237
+ ) as Challenge.Challenge[]
238
+
239
+ const result = transport.setCredential(jsonRpcRequest, 'credential', {
240
+ challenge: nativeChallenge,
241
+ })
242
+ const headers = new Headers(result.headers)
243
+
244
+ expect(headers.get('Authorization')).toBe('credential')
245
+ expect(result.body).toBe(jsonRpcRequest.body)
246
+ })
247
+
248
+ test('marks x402 challenges with the brand the evm charge client reads', async () => {
249
+ // Provenance guard: `evm/client/Charge.ts` recognizes x402 challenges via the same
250
+ // brand the x402 adapter stamps. If the marking site drifts, EVM x402 charges misroute.
251
+ const [x402Challenge] = await Transport.http().getChallenges!(
252
+ new Response(null, {
253
+ status: 402,
254
+ headers: {
255
+ 'PAYMENT-REQUIRED': x402_Header.encodePaymentRequired(x402PaymentRequired),
256
+ },
257
+ }),
258
+ )
259
+ expect(x402_ChallengeBrand.is(x402Challenge)).toBe(true)
260
+ })
261
+
262
+ test('removes stale credential headers before setting the retry credential', async () => {
220
263
  const transport = Transport.http()
221
- const x402Challenge = Transport.http().getChallenges!(
264
+ const [x402Challenge] = await transport.getChallenges!(
222
265
  new Response(null, {
223
266
  status: 402,
224
267
  headers: {
225
268
  'PAYMENT-REQUIRED': x402_Header.encodePaymentRequired(x402PaymentRequired),
226
269
  },
227
270
  }),
228
- )[0]
271
+ )
229
272
 
230
273
  const result = transport.setCredential(
231
274
  {
@@ -258,191 +301,195 @@ describe('http', () => {
258
301
  })
259
302
  })
260
303
 
261
- describe('mcp', () => {
262
- const mcpRequest: Mcp.Request = {
263
- method: 'tools/call',
264
- params: {
265
- name: 'test-tool',
304
+ describe('http (MCP-over-HTTP)', () => {
305
+ // An MCP-over-HTTP call: a JSON-RPC POST whose response carries the -32042 challenge.
306
+ const jsonRpcRequest = {
307
+ method: 'POST',
308
+ headers: { accept: 'application/json, text/event-stream' },
309
+ body: JSON.stringify({
310
+ jsonrpc: '2.0',
311
+ id: 1,
312
+ method: 'tools/call',
313
+ params: { name: 'premium' },
314
+ }),
315
+ } satisfies RequestInit
316
+ const errorMessage = {
317
+ jsonrpc: '2.0',
318
+ id: 1,
319
+ error: {
320
+ code: Mcp.paymentRequiredCode,
321
+ message: 'Payment Required',
322
+ data: { challenges: [challenge] },
266
323
  },
267
324
  }
268
-
269
- describe('isPaymentRequired', () => {
270
- test('returns true for payment required error', () => {
271
- const transport = Transport.mcp()
272
- const response: Mcp.Response = {
273
- jsonrpc: '2.0',
274
- id: 1,
275
- error: {
276
- code: Mcp.paymentRequiredCode,
277
- message: 'Payment Required',
278
- data: {
279
- httpStatus: 402,
280
- challenges: [challenge],
281
- },
282
- },
283
- }
284
-
285
- expect(transport.isPaymentRequired(response)).toBe(true)
325
+ const jsonBody = () =>
326
+ new Response(JSON.stringify(errorMessage), { headers: { 'content-type': 'application/json' } })
327
+ const sseBody = () =>
328
+ new Response(`event: message\ndata: ${JSON.stringify(errorMessage)}\n\n`, {
329
+ headers: { 'content-type': 'text/event-stream' },
286
330
  })
287
331
 
288
- test('returns false for success response', () => {
289
- const transport = Transport.mcp()
290
- const response: Mcp.Response = {
291
- jsonrpc: '2.0',
292
- id: 1,
293
- result: { content: [] },
294
- }
295
-
296
- expect(transport.isPaymentRequired(response)).toBe(false)
332
+ test('detects -32042 in a JSON body (JSON-RPC request)', async () => {
333
+ expect(await Transport.http().isPaymentRequired(jsonBody(), jsonRpcRequest)).toBe(true)
334
+ })
335
+ test('ignores a JSON-RPC response for a different request id', async () => {
336
+ const response = new Response(JSON.stringify({ ...errorMessage, id: 2 }), {
337
+ headers: { 'content-type': 'application/json' },
297
338
  })
298
339
 
299
- test('returns false for other error codes', () => {
300
- const transport = Transport.mcp()
301
- const response: Mcp.Response = {
302
- jsonrpc: '2.0',
303
- id: 1,
304
- error: {
305
- code: -32600,
306
- message: 'Invalid Request',
307
- },
308
- }
309
-
310
- expect(transport.isPaymentRequired(response)).toBe(false)
311
- })
340
+ expect(await Transport.http().isPaymentRequired(response, jsonRpcRequest)).toBe(false)
312
341
  })
313
-
314
- describe('getChallenge', () => {
315
- test('default', () => {
316
- const transport = Transport.mcp()
317
- const response: Mcp.Response = {
318
- jsonrpc: '2.0',
319
- id: 1,
320
- error: {
321
- code: Mcp.paymentRequiredCode,
322
- message: 'Payment Required',
323
- data: {
324
- httpStatus: 402,
325
- challenges: [challenge],
326
- },
342
+ test('detects -32042 in an SSE body', async () => {
343
+ expect(await Transport.http().isPaymentRequired(sseBody(), jsonRpcRequest)).toBe(true)
344
+ })
345
+ test('does not scan past the first SSE data event', async () => {
346
+ const notification = { jsonrpc: '2.0', method: 'notifications/progress', params: {} }
347
+ const response = new Response(
348
+ `event: message\ndata: ${JSON.stringify(notification)}\n\n` +
349
+ `event: message\ndata: ${JSON.stringify(errorMessage)}\n\n`,
350
+ { headers: { 'content-type': 'text/event-stream' } },
351
+ )
352
+
353
+ expect(await Transport.http().isPaymentRequired(response, jsonRpcRequest)).toBe(false)
354
+ })
355
+ test('detects -32042 in an open SSE stream without waiting for stream close', async () => {
356
+ const encoder = new TextEncoder()
357
+ let timeout: ReturnType<typeof setTimeout> | undefined
358
+ const openSse = new Response(
359
+ new ReadableStream({
360
+ start(controller) {
361
+ controller.enqueue(
362
+ encoder.encode(`event: message\ndata: ${JSON.stringify(errorMessage)}\n\n`),
363
+ )
327
364
  },
328
- }
365
+ }),
366
+ { headers: { 'content-type': 'text/event-stream' } },
367
+ )
368
+
369
+ try {
370
+ const result = await Promise.race([
371
+ Transport.http().isPaymentRequired(openSse, jsonRpcRequest),
372
+ new Promise<'timeout'>((resolve) => {
373
+ timeout = setTimeout(() => resolve('timeout'), 100)
374
+ }),
375
+ ])
329
376
 
330
- expect(transport.getChallenge(response)).toMatchObject({
331
- expires: '2025-01-01T00:00:00.000Z',
332
- id: expect.any(String),
333
- intent: 'charge',
334
- method: 'tempo',
335
- realm: 'api.example.com',
336
- request: {
337
- amount: '1000',
338
- currency: '0x20c0000000000000000000000000000000000001',
339
- recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
340
- },
341
- })
377
+ expect(result).toBe(true)
378
+ } finally {
379
+ if (timeout) clearTimeout(timeout)
380
+ }
381
+ })
382
+ test('does not read the body for a non-JSON-RPC request', async () => {
383
+ expect(
384
+ await Transport.http().isPaymentRequired(sseBody(), { method: 'POST', body: 'plain' }),
385
+ ).toBe(false)
386
+ })
387
+ test('does not treat generic JSON-RPC as MCP-over-HTTP', async () => {
388
+ const request = {
389
+ method: 'POST',
390
+ headers: { accept: 'application/json' },
391
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [] }),
392
+ } satisfies RequestInit
393
+
394
+ expect(await Transport.http().isPaymentRequired(sseBody(), request)).toBe(false)
395
+ })
396
+ test('ignores a 200 success', async () => {
397
+ const ok = new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }), {
398
+ headers: { 'content-type': 'application/json' },
342
399
  })
343
-
344
- test('throws for success response', () => {
345
- const transport = Transport.mcp()
346
- const response: Mcp.Response = {
347
- jsonrpc: '2.0',
348
- id: 1,
349
- result: { content: [] },
350
- }
351
-
352
- expect(() => transport.getChallenge(response)).toThrow('Response is not an error')
400
+ expect(await Transport.http().isPaymentRequired(ok, jsonRpcRequest)).toBe(false)
401
+ })
402
+ test('getChallenges extracts the MCP challenge (via the mcp protocol)', async () => {
403
+ const challenges = await Transport.http().getChallenges!(sseBody(), jsonRpcRequest)
404
+ expect(challenges.map((entry) => entry.id)).toEqual([challenge.id])
405
+ })
406
+ test('setCredential routes the MCP challenge into the JSON-RPC _meta', async () => {
407
+ const transport = Transport.http()
408
+ const [mcpChallenge] = await transport.getChallenges!(sseBody(), jsonRpcRequest)
409
+ const result = transport.setCredential(jsonRpcRequest, Credential.serialize(credential), {
410
+ challenge: mcpChallenge,
411
+ }) as RequestInit
412
+ const parsed = JSON.parse(result.body as string)
413
+ expect(parsed.params['_meta'][Mcp.credentialMetaKey]).toMatchObject({
414
+ payload: { type: 'transaction' },
353
415
  })
354
-
355
- test('throws when no challenges in error', () => {
356
- const transport = Transport.mcp()
357
- const response: Mcp.Response = {
358
- jsonrpc: '2.0',
359
- id: 1,
360
- error: {
361
- code: Mcp.paymentRequiredCode,
362
- message: 'Payment Required',
363
- data: {
364
- httpStatus: 402,
365
- challenges: [],
366
- },
367
- },
368
- }
369
-
370
- expect(() => transport.getChallenge(response)).toThrow('No challenge in error response')
416
+ })
417
+ test('detects -32042 in a JSON body containing a "data:" substring', async () => {
418
+ const body = JSON.stringify({
419
+ jsonrpc: '2.0',
420
+ id: 1,
421
+ error: {
422
+ code: Mcp.paymentRequiredCode,
423
+ message: 'Payment Required',
424
+ data: { challenges: [{ ...challenge, realm: 'data:text/plain,x' }] },
425
+ },
371
426
  })
427
+ const response = new Response(body, { headers: { 'content-type': 'application/json' } })
428
+ expect(await Transport.http().isPaymentRequired(response, jsonRpcRequest)).toBe(true)
372
429
  })
373
-
374
- describe('getChallenges', () => {
375
- test('returns all MCP challenges', () => {
376
- const transport = Transport.mcp()
377
- const response: Mcp.Response = {
430
+ test('ignores malformed payment challenge data', async () => {
431
+ const response = new Response(
432
+ JSON.stringify({
378
433
  jsonrpc: '2.0',
379
434
  id: 1,
380
435
  error: {
381
436
  code: Mcp.paymentRequiredCode,
382
437
  message: 'Payment Required',
383
- data: {
384
- httpStatus: 402,
385
- challenges: [challenge, { ...challenge, id: 'alternate', method: 'stripe' }],
386
- },
438
+ data: { challenges: [{ ...challenge, realm: undefined }] },
387
439
  },
388
- }
440
+ }),
441
+ { headers: { 'content-type': 'application/json' } },
442
+ )
389
443
 
390
- expect(transport.getChallenges?.(response).map((entry) => entry.id)).toEqual([
391
- challenge.id,
392
- 'alternate',
393
- ])
394
- })
444
+ expect(await Transport.http().isPaymentRequired(response, jsonRpcRequest)).toBe(false)
395
445
  })
446
+ test('detects -32042 for any JSON-RPC method (no method allowlist)', async () => {
447
+ const request = {
448
+ method: 'POST',
449
+ headers: { accept: 'application/json, text/event-stream' },
450
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'custom/paidMethod', params: {} }),
451
+ } satisfies RequestInit
452
+ expect(await Transport.http().isPaymentRequired(sseBody(), request)).toBe(true)
453
+ })
454
+ })
396
455
 
397
- describe('setCredential', () => {
398
- test('default', () => {
399
- const transport = Transport.mcp()
400
- const serialized = Credential.serialize(credential)
456
+ describe('mcp', () => {
457
+ const mcpRequest: Mcp.Request = {
458
+ method: 'tools/call',
459
+ params: {
460
+ name: 'test-tool',
461
+ },
462
+ }
401
463
 
402
- expect(transport.setCredential(mcpRequest, serialized)).toMatchObject({
403
- method: 'tools/call',
404
- params: {
405
- _meta: {
406
- 'org.paymentauth/credential': {
407
- challenge: {
408
- expires: '2025-01-01T00:00:00.000Z',
409
- id: expect.any(String),
410
- intent: 'charge',
411
- method: 'tempo',
412
- realm: 'api.example.com',
413
- request: {
414
- amount: '1000',
415
- currency: '0x20c0000000000000000000000000000000000001',
416
- recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
417
- },
418
- },
419
- payload: {
420
- signature: '0xabc123',
421
- type: 'transaction',
422
- },
423
- },
424
- },
425
- name: 'test-tool',
426
- },
427
- })
428
- })
464
+ const mcpResponse: Mcp.Response = {
465
+ jsonrpc: '2.0',
466
+ id: 1,
467
+ error: {
468
+ code: Mcp.paymentRequiredCode,
469
+ message: 'Payment Required',
470
+ data: {
471
+ httpStatus: 402,
472
+ challenges: [challenge],
473
+ },
474
+ },
475
+ }
429
476
 
430
- test('preserves existing _meta', () => {
431
- const transport = Transport.mcp()
432
- const serialized = Credential.serialize(credential)
433
- const requestWithMeta: Mcp.Request = {
434
- ...mcpRequest,
435
- params: {
436
- ...mcpRequest.params,
437
- _meta: {
438
- existingKey: 'existingValue',
439
- },
440
- },
441
- }
477
+ test('extracts payment-required challenges from JSON-RPC errors', async () => {
478
+ const transport = Transport.mcp()
479
+
480
+ expect(await transport.isPaymentRequired(mcpResponse)).toBe(true)
481
+ expect((await transport.getChallenges?.(mcpResponse))?.map((entry) => entry.id)).toEqual([
482
+ challenge.id,
483
+ ])
484
+ })
442
485
 
443
- const result = transport.setCredential(requestWithMeta, serialized)
486
+ test('sets credentials in JSON-RPC _meta', () => {
487
+ const result = Transport.mcp().setCredential(mcpRequest, Credential.serialize(credential))
444
488
 
445
- expect(result.params?._meta?.existingKey).toBe('existingValue')
489
+ expect(result.params?.['_meta']?.[Mcp.credentialMetaKey]).toMatchObject({
490
+ payload: {
491
+ type: 'transaction',
492
+ },
446
493
  })
447
494
  })
448
495
  })