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
@@ -11,6 +11,8 @@ import type { MaybePromise } from '../internal/types.js'
11
11
  import type * as Method from '../Method.js'
12
12
  import * as PaymentRequest from '../PaymentRequest.js'
13
13
  import type * as Receipt from '../Receipt.js'
14
+ import * as x402_Header from '../x402/Header.js'
15
+ import * as x402_Types from '../x402/Types.js'
14
16
  import * as z from '../zod.js'
15
17
  import * as Html from './internal/html/config.js'
16
18
  import { serviceWorker } from './internal/html/serviceWorker.gen.js'
@@ -790,7 +792,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
790
792
  : staticMeta
791
793
 
792
794
  // Extract credential once — getCredential may have side effects (e.g. SSE transports).
793
- const [credential, credentialError] = (() => {
795
+ let [credential, credentialError] = (() => {
794
796
  try {
795
797
  const credential = transport.getCredential(input) as Credential.Credential | null
796
798
  return [credential ? hydrateCredentialMeta(credential) : null, undefined] as const
@@ -898,6 +900,21 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
898
900
  if ('response' in routeChallenge) return { challenge: routeChallenge.response, status: 402 }
899
901
  const { challenge, parsedRequest, request } = routeChallenge
900
902
 
903
+ if (credential && transport.bindCredential) {
904
+ try {
905
+ credential = hydrateCredentialMeta(
906
+ (await transport.bindCredential({
907
+ challenge,
908
+ credential,
909
+ input,
910
+ })) as Credential.Credential,
911
+ )
912
+ } catch (e) {
913
+ credential = null
914
+ credentialError = e as Error
915
+ }
916
+ }
917
+
901
918
  // Credential was provided but malformed
902
919
  if (credentialError) {
903
920
  const reason = getSafeCredentialReason(credentialError)
@@ -2004,6 +2021,29 @@ type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transpor
2004
2021
  }
2005
2022
  }
2006
2023
 
2024
+ const paymentAuthChallengeHeader = 'WWW-Authenticate'
2025
+
2026
+ const challengeHeaderMerges = [
2027
+ {
2028
+ name: paymentAuthChallengeHeader,
2029
+ values: (context) =>
2030
+ context.challengeEntries
2031
+ .map((entry) =>
2032
+ (entry.result.challenge as Response).headers.get(paymentAuthChallengeHeader),
2033
+ )
2034
+ .filter((value): value is string => value !== null),
2035
+ merge: (values) => values,
2036
+ },
2037
+ {
2038
+ name: x402_Types.paymentRequiredHeader,
2039
+ values: (context) =>
2040
+ context.negotiatedChallengeResponses
2041
+ .map((response) => response.headers.get(x402_Types.paymentRequiredHeader))
2042
+ .filter((value): value is string => value !== null),
2043
+ merge: mergeX402PaymentRequiredHeaders,
2044
+ },
2045
+ ] satisfies readonly ChallengeHeaderMerge[]
2046
+
2007
2047
  /** An entry for `compose()`: a method reference, handler function ref, or string key paired with its options. */
2008
2048
  type ComposeEntry<methods extends readonly Method.AnyServer[]> =
2009
2049
  | {
@@ -2172,7 +2212,7 @@ export function compose(
2172
2212
  if (result?.status !== 402) continue
2173
2213
 
2174
2214
  const response = result.challenge as Response
2175
- const wwwAuth = response.headers.get('WWW-Authenticate')
2215
+ const wwwAuth = response.headers.get(paymentAuthChallengeHeader)
2176
2216
  if (!wwwAuth) continue
2177
2217
 
2178
2218
  entries.push({
@@ -2199,14 +2239,39 @@ export function compose(
2199
2239
  }
2200
2240
  })()
2201
2241
 
2202
- // Merge WWW-Authenticate headers from all 402 responses.
2242
+ const challengeResponses = results.flatMap((result) =>
2243
+ result.status === 402 ? [result.challenge as Response] : [],
2244
+ )
2245
+ const unnegotiatedX402Responses =
2246
+ input.headers.has('Accept-Payment') || challengeEntries.length === 0
2247
+ ? []
2248
+ : challengeResponses.filter(
2249
+ (response) =>
2250
+ response.headers.has(x402_Types.paymentRequiredHeader) &&
2251
+ !response.headers.has(paymentAuthChallengeHeader),
2252
+ )
2253
+ const negotiatedChallengeResponses =
2254
+ challengeEntries.length > 0
2255
+ ? [
2256
+ ...challengeEntries.map((entry) => entry.result.challenge as Response),
2257
+ ...unnegotiatedX402Responses,
2258
+ ]
2259
+ : challengeResponses
2260
+
2261
+ // Merge challenge headers from the negotiated 402 responses.
2203
2262
  const mergedHeaders = new Headers()
2204
2263
  mergedHeaders.set('Cache-Control', 'no-store')
2205
2264
 
2206
- for (const entry of challengeEntries) {
2207
- const response = entry.result.challenge as Response
2208
- const wwwAuth = response.headers.get('WWW-Authenticate')
2209
- if (wwwAuth) mergedHeaders.append('WWW-Authenticate', wwwAuth)
2265
+ for (const header of challengeHeaderMerges) {
2266
+ for (const value of header.merge(
2267
+ header.values({
2268
+ challengeEntries,
2269
+ challengeResponses,
2270
+ negotiatedChallengeResponses,
2271
+ }),
2272
+ )) {
2273
+ mergedHeaders.append(header.name, value)
2274
+ }
2210
2275
  }
2211
2276
 
2212
2277
  // Collect html-enabled handlers and their challenges
@@ -2257,11 +2322,15 @@ export function compose(
2257
2322
  }
2258
2323
  }
2259
2324
 
2260
- // Non-HTML fallback: use first handler's body
2325
+ // Non-HTML fallback: prefer the first Payment-auth body, otherwise use
2326
+ // the first transport-specific 402 body.
2261
2327
  let body: string | null = null
2262
- for (const entry of challengeEntries) {
2328
+ const bodyResponses =
2329
+ challengeEntries.length > 0
2330
+ ? challengeEntries.map((entry) => entry.result.challenge as Response)
2331
+ : challengeResponses
2332
+ for (const response of bodyResponses) {
2263
2333
  if (!body) {
2264
- const response = entry.result.challenge as Response
2265
2334
  const contentType = response.headers.get('Content-Type')
2266
2335
  if (contentType) mergedHeaders.set('Content-Type', contentType)
2267
2336
  body = await response.text()
@@ -2276,6 +2345,50 @@ export function compose(
2276
2345
  }
2277
2346
  }
2278
2347
 
2348
+ type ChallengeHeaderMerge = {
2349
+ name: string
2350
+ values(context: {
2351
+ challengeEntries: readonly {
2352
+ handler: ConfiguredHandler
2353
+ challenge: Challenge.Challenge
2354
+ result: Extract<MethodFn.Response<Transport.Http>, { status: 402 }>
2355
+ }[]
2356
+ challengeResponses: readonly Response[]
2357
+ negotiatedChallengeResponses: readonly Response[]
2358
+ }): readonly string[]
2359
+ merge(values: readonly string[]): readonly string[]
2360
+ }
2361
+
2362
+ function mergeX402PaymentRequiredHeaders(values: readonly string[]): readonly string[] {
2363
+ if (values.length === 0) return []
2364
+ const [first, ...rest] = values.map((value) => x402_Header.decodePaymentRequired(value))
2365
+ if (!first) throw new Error('Expected at least one x402 payment-required header.')
2366
+ const incompatible = rest.some(
2367
+ (value) =>
2368
+ !isDeepStrictEqual(value.resource, first.resource) ||
2369
+ !isDeepStrictEqual(value.extensions, first.extensions),
2370
+ )
2371
+ if (incompatible)
2372
+ return [
2373
+ x402_Header.encodePaymentRequired({
2374
+ ...first,
2375
+ error: [first.error, 'Cannot merge x402 payment requirements with different resources.']
2376
+ .filter((value): value is string => value !== undefined && value.length > 0)
2377
+ .join('; '),
2378
+ }),
2379
+ ]
2380
+ const error = [first.error, ...rest.map((value) => value.error)]
2381
+ .filter((value): value is string => value !== undefined && value.length > 0)
2382
+ .join('; ')
2383
+ return [
2384
+ x402_Header.encodePaymentRequired({
2385
+ ...first,
2386
+ accepts: [first.accepts, ...rest.map((value) => value.accepts)].flat(),
2387
+ ...(error ? { error } : {}),
2388
+ }),
2389
+ ]
2390
+ }
2391
+
2279
2392
  /**
2280
2393
  * Wraps a payment handler to create a Node.js HTTP listener.
2281
2394
  *
@@ -2316,7 +2429,7 @@ export function toNodeListener(
2316
2429
  }
2317
2430
 
2318
2431
  const wrapped = result.withReceipt(new globalThis.Response()) as globalThis.Response
2319
- res.setHeader('Payment-Receipt', wrapped.headers.get('Payment-Receipt')!)
2432
+ for (const [name, value] of wrapped.headers) res.setHeader(name, value)
2320
2433
  }
2321
2434
 
2322
2435
  return result
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
3
 
4
- import { NodeListener, Request } from 'mppx/server'
4
+ import { Mppx, NodeListener, Request } from 'mppx/server'
5
5
  import { afterEach, describe, expect, test } from 'vp/test'
6
6
  import * as Http from '~test/Http.js'
7
7
 
@@ -185,6 +185,33 @@ describe('toNodeListener', () => {
185
185
  expect(await response.json()).toEqual({ path: '/hello' })
186
186
  })
187
187
 
188
+ test('copies all receipt response headers for successful payment handlers', async () => {
189
+ const handler = Mppx.toNodeListener(async () => ({
190
+ status: 200,
191
+ withReceipt(response?: Response) {
192
+ if (!response) {
193
+ const error = new Error('withReceipt() requires a response argument')
194
+ error.name = 'MissingReceiptResponseError'
195
+ throw error
196
+ }
197
+ const headers = new Headers(response?.headers)
198
+ headers.set('Payment-Receipt', 'receipt')
199
+ headers.set('PAYMENT-RESPONSE', 'x402-response')
200
+ return new Response(response?.body, { headers })
201
+ },
202
+ }))
203
+
204
+ server = await Http.createServer(async (req, res) => {
205
+ await handler(req, res)
206
+ res.end('ok')
207
+ })
208
+
209
+ const response = await fetch(server.url)
210
+ expect(response.headers.get('Payment-Receipt')).toBe('receipt')
211
+ expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
212
+ expect(await response.text()).toBe('ok')
213
+ })
214
+
188
215
  test('forwards request method', async () => {
189
216
  const handler = Request.toNodeListener(async (request) => {
190
217
  return Response.json({ method: request.method })
@@ -408,6 +408,7 @@ describe('http', () => {
408
408
  }).toMatchInlineSnapshot(`
409
409
  {
410
410
  "headers": {
411
+ "cache-control": "private",
411
412
  "content-type": "text/plain;charset=UTF-8",
412
413
  "payment-receipt": "eyJtZXRob2QiOiJ0ZW1wbyIsInJlZmVyZW5jZSI6IjB4dHhoYXNoIiwic3RhdHVzIjoic3VjY2VzcyIsInRpbWVzdGFtcCI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiJ9",
413
414
  },
@@ -415,6 +416,24 @@ describe('http', () => {
415
416
  }
416
417
  `)
417
418
  })
419
+
420
+ test('preserves existing cache directives while marking receipts private', () => {
421
+ const transport = Transport.http()
422
+ const originalResponse = new Response('OK', {
423
+ status: 200,
424
+ headers: { 'Cache-Control': 'max-age=60' },
425
+ })
426
+
427
+ const response = transport.respondReceipt({
428
+ credential,
429
+ input: new Request('https://example.com'),
430
+ receipt,
431
+ response: originalResponse,
432
+ challengeId: challenge.id,
433
+ })
434
+
435
+ expect(response.headers.get('Cache-Control')).toBe('max-age=60, private')
436
+ })
418
437
  })
419
438
  })
420
439
 
@@ -26,6 +26,18 @@ export type Transport<
26
26
  name: string
27
27
  /** Captures the transport request into an immutable verification snapshot. */
28
28
  captureRequest?: ((input: input) => MaybePromise<Method.CapturedRequest>) | undefined
29
+ /**
30
+ * Rebinds a transport-native credential to the route challenge after request
31
+ * normalization. Transports with non-Payment-auth wire formats can parse their
32
+ * payload early, then attach the canonical mppx challenge here.
33
+ */
34
+ bindCredential?:
35
+ | ((options: {
36
+ challenge: Challenge.Challenge
37
+ credential: Credential.Credential
38
+ input: input
39
+ }) => MaybePromise<Credential.Credential>)
40
+ | undefined
29
41
  /**
30
42
  * Extracts credential from the transport input.
31
43
  * Returns `null` if no credential was provided, or throws if malformed.
@@ -196,6 +208,7 @@ export function http(): Http {
196
208
 
197
209
  respondReceipt({ receipt, response }) {
198
210
  const headers = new Headers(response.headers)
211
+ headers.set('Cache-Control', withPrivateCacheControl(headers.get('Cache-Control')))
199
212
  headers.set('Payment-Receipt', Receipt.serialize(receipt))
200
213
  return new Response(response.body, {
201
214
  status: response.status,
@@ -206,6 +219,13 @@ export function http(): Http {
206
219
  })
207
220
  }
208
221
 
222
+ function withPrivateCacheControl(value: string | null): string {
223
+ if (!value) return 'private'
224
+ const directives = value.split(',').map((directive) => directive.trim().toLowerCase())
225
+ if (directives.includes('private')) return value
226
+ return `${value}, private`
227
+ }
228
+
209
229
  /**
210
230
  * MCP transport for server-side payment handling with raw JSON-RPC.
211
231
  *
@@ -1,6 +1,6 @@
1
1
  export * as Expires from '../Expires.js'
2
2
  export * as Store from '../Store.js'
3
- export { stripe, tempo } from './Methods.js'
3
+ export { evm, stripe, tempo } from './Methods.js'
4
4
  export * as Mppx from './Mppx.js'
5
5
  export * as NodeListener from './NodeListener.js'
6
6
  export * as Request from './Request.js'
@@ -4,13 +4,18 @@ import { afterEach, describe, expect, test, vi } from 'vp/test'
4
4
  import * as Http from '~test/Http.js'
5
5
 
6
6
  import type { StripeClient } from '../internal/types.js'
7
+ import type { charge as StripeCharge } from './Charge.js'
7
8
 
8
9
  const realm = 'api.example.com'
9
10
  const secretKey = 'test-secret-key'
10
11
 
11
12
  let httpServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
12
13
 
13
- afterEach(() => httpServer?.close())
14
+ afterEach(() => {
15
+ httpServer?.close()
16
+ httpServer = undefined
17
+ vi.restoreAllMocks()
18
+ })
14
19
 
15
20
  function createMockStripeClient(
16
21
  overrides?: Partial<{ status: string; id: string; throws: boolean }>,
@@ -126,6 +131,215 @@ describe('stripe.charge with client', () => {
126
131
  expect(params.metadata.mpp_is_mpp).toBe('true')
127
132
  })
128
133
 
134
+ test('behavior: applies Connect settlement parameters in client call', async () => {
135
+ const { client, create } = createMockStripeClient()
136
+
137
+ const server = Mppx.create({
138
+ methods: [
139
+ stripe.charge({
140
+ client,
141
+ connect({ request }) {
142
+ expect(request.amount).toBe('100')
143
+ return {
144
+ applicationFeeAmount: 12,
145
+ onBehalfOf: 'acct_merchant',
146
+ stripeAccount: 'acct_connected',
147
+ transferData: { amount: 88, destination: 'acct_destination' },
148
+ transferGroup: 'order_123',
149
+ }
150
+ },
151
+ networkId: 'internal',
152
+ paymentMethodTypes: ['card'],
153
+ }),
154
+ ],
155
+ realm,
156
+ secretKey,
157
+ })
158
+
159
+ const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
160
+ const firstResult = await handle(new Request('https://example.com'))
161
+ expect(firstResult.status).toBe(402)
162
+ if (firstResult.status !== 402) throw new Error()
163
+
164
+ const challenge = Challenge.fromResponse(firstResult.challenge)
165
+ expect(challenge.request).not.toHaveProperty('connect')
166
+ expect(challenge.request.methodDetails).not.toHaveProperty('applicationFeeAmount')
167
+ expect(challenge.request.methodDetails).not.toHaveProperty('stripeAccount')
168
+
169
+ const credential = Credential.from({
170
+ challenge,
171
+ payload: { spt: 'spt_test_token' },
172
+ })
173
+
174
+ const result = await handle(
175
+ new Request('https://example.com', {
176
+ headers: { Authorization: Credential.serialize(credential) },
177
+ }),
178
+ )
179
+
180
+ expect(result.status).toBe(200)
181
+ const [params, options] = create.mock.calls[0]!
182
+ expect(params).toMatchObject({
183
+ application_fee_amount: 12,
184
+ on_behalf_of: 'acct_merchant',
185
+ transfer_data: { amount: 88, destination: 'acct_destination' },
186
+ transfer_group: 'order_123',
187
+ })
188
+ expect(options).toMatchObject({ stripeAccount: 'acct_connected' })
189
+ })
190
+
191
+ test('behavior: applies Connect settlement parameters in secretKey call', async () => {
192
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
193
+ new Response(JSON.stringify({ id: 'pi_fetch_123', status: 'succeeded' }), {
194
+ status: 200,
195
+ }),
196
+ )
197
+
198
+ const server = Mppx.create({
199
+ methods: [
200
+ stripe.charge({
201
+ connect: {
202
+ applicationFeeAmount: 12,
203
+ onBehalfOf: 'acct_merchant',
204
+ stripeAccount: 'acct_connected',
205
+ transferData: { amount: 88, destination: 'acct_destination' },
206
+ transferGroup: 'order_123',
207
+ },
208
+ networkId: 'internal',
209
+ paymentMethodTypes: ['card'],
210
+ secretKey,
211
+ }),
212
+ ],
213
+ realm,
214
+ secretKey,
215
+ })
216
+
217
+ const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
218
+ const firstResult = await handle(new Request('https://example.com'))
219
+ expect(firstResult.status).toBe(402)
220
+ if (firstResult.status !== 402) throw new Error()
221
+
222
+ const credential = Credential.from({
223
+ challenge: Challenge.fromResponse(firstResult.challenge),
224
+ payload: { spt: 'spt_test_token' },
225
+ })
226
+ const result = await handle(
227
+ new Request('https://example.com', {
228
+ headers: { Authorization: Credential.serialize(credential) },
229
+ }),
230
+ )
231
+
232
+ expect(result.status).toBe(200)
233
+ expect(fetchMock).toHaveBeenCalledOnce()
234
+ const [input, init] = fetchMock.mock.calls[0]!
235
+ expect(input).toBe('https://api.stripe.com/v1/payment_intents')
236
+ const headers = new Headers(init?.headers)
237
+ expect(headers.get('Stripe-Account')).toBe('acct_connected')
238
+ const body = init?.body as URLSearchParams
239
+ expect(body.get('application_fee_amount')).toBe('12')
240
+ expect(body.get('on_behalf_of')).toBe('acct_merchant')
241
+ expect(body.get('transfer_data[amount]')).toBe('88')
242
+ expect(body.get('transfer_data[destination]')).toBe('acct_destination')
243
+ expect(body.get('transfer_group')).toBe('order_123')
244
+ })
245
+
246
+ test('error: surfaces Connect PaymentIntent creation failures', async () => {
247
+ const { client } = createMockStripeClient({ throws: true })
248
+
249
+ const server = Mppx.create({
250
+ methods: [
251
+ stripe.charge({
252
+ client,
253
+ connect: { stripeAccount: 'acct_connected' },
254
+ networkId: 'internal',
255
+ paymentMethodTypes: ['card'],
256
+ }),
257
+ ],
258
+ realm,
259
+ secretKey,
260
+ })
261
+
262
+ httpServer = await Http.createServer(async (req, res) => {
263
+ const result = await Mppx.toNodeListener(
264
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
265
+ )(req, res)
266
+ if (result.status === 402) return
267
+ res.end('OK')
268
+ })
269
+
270
+ const response = await fetch(httpServer.url)
271
+ const challenge = Challenge.fromResponse(response)
272
+ const credential = Credential.from({
273
+ challenge,
274
+ payload: { spt: 'spt_test_token' },
275
+ })
276
+
277
+ const paidResponse = await fetch(httpServer.url, {
278
+ headers: { Authorization: Credential.serialize(credential) },
279
+ })
280
+ expect(paidResponse.status).toBe(402)
281
+ const body = (await paidResponse.json()) as { detail: string }
282
+ expect(body.detail).toContain('Stripe PaymentIntent failed')
283
+ })
284
+
285
+ const invalidConnectCases: readonly {
286
+ name: string
287
+ connect: StripeCharge.ConnectSettlement
288
+ }[] = [
289
+ { name: 'empty stripeAccount', connect: { stripeAccount: '' } },
290
+ { name: 'fee exceeds amount', connect: { applicationFeeAmount: 101 } },
291
+ { name: 'negative fee', connect: { applicationFeeAmount: -1 } },
292
+ {
293
+ name: 'empty transfer destination',
294
+ connect: { transferData: { destination: '' } },
295
+ },
296
+ {
297
+ name: 'missing transfer destination',
298
+ connect: { transferData: {} } as StripeCharge.ConnectSettlement,
299
+ },
300
+ {
301
+ name: 'transfer amount exceeds amount',
302
+ connect: { transferData: { amount: 101, destination: 'acct_destination' } },
303
+ },
304
+ ]
305
+
306
+ for (const { connect, name } of invalidConnectCases) {
307
+ test(`error: rejects invalid Connect settlement parameters (${name})`, async () => {
308
+ const { client, create } = createMockStripeClient()
309
+
310
+ const server = Mppx.create({
311
+ methods: [
312
+ stripe.charge({
313
+ client,
314
+ connect,
315
+ networkId: 'internal',
316
+ paymentMethodTypes: ['card'],
317
+ }),
318
+ ],
319
+ realm,
320
+ secretKey,
321
+ })
322
+
323
+ const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
324
+ const firstResult = await handle(new Request('https://example.com'))
325
+ expect(firstResult.status).toBe(402)
326
+ if (firstResult.status !== 402) throw new Error()
327
+
328
+ const credential = Credential.from({
329
+ challenge: Challenge.fromResponse(firstResult.challenge),
330
+ payload: { spt: 'spt_test_token' },
331
+ })
332
+ const result = await handle(
333
+ new Request('https://example.com', {
334
+ headers: { Authorization: Credential.serialize(credential) },
335
+ }),
336
+ )
337
+
338
+ expect(result.status).toBe(402)
339
+ expect(create).not.toHaveBeenCalled()
340
+ })
341
+ }
342
+
129
343
  test('behavior: rejects when client throws', async () => {
130
344
  const { client } = createMockStripeClient({ throws: true })
131
345