mppx 0.8.14 → 0.8.17

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 (507) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +40 -35
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Constants.d.ts +17 -17
  6. package/dist/Constants.d.ts.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Errors.d.ts +3 -3
  9. package/dist/Errors.d.ts.map +1 -1
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/Method.d.ts +35 -5
  12. package/dist/Method.d.ts.map +1 -1
  13. package/dist/Method.js +7 -22
  14. package/dist/Method.js.map +1 -1
  15. package/dist/Store.d.ts +19 -0
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +43 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/attestation/Client.d.ts +17 -0
  20. package/dist/attestation/Client.d.ts.map +1 -0
  21. package/dist/attestation/Client.js +28 -0
  22. package/dist/attestation/Client.js.map +1 -0
  23. package/dist/attestation/Constants.d.ts +15 -0
  24. package/dist/attestation/Constants.d.ts.map +1 -0
  25. package/dist/attestation/Constants.js +15 -0
  26. package/dist/attestation/Constants.js.map +1 -0
  27. package/dist/attestation/Server.d.ts +4 -0
  28. package/dist/attestation/Server.d.ts.map +1 -0
  29. package/dist/attestation/Server.js +8 -0
  30. package/dist/attestation/Server.js.map +1 -0
  31. package/dist/attestation/Types.d.ts +76 -0
  32. package/dist/attestation/Types.d.ts.map +1 -0
  33. package/dist/attestation/Types.js +2 -0
  34. package/dist/attestation/Types.js.map +1 -0
  35. package/dist/attestation/index.d.ts +6 -0
  36. package/dist/attestation/index.d.ts.map +1 -0
  37. package/dist/attestation/index.js +6 -0
  38. package/dist/attestation/index.js.map +1 -0
  39. package/dist/attestation/internal/HttpMessageSignature.d.ts +73 -0
  40. package/dist/attestation/internal/HttpMessageSignature.d.ts.map +1 -0
  41. package/dist/attestation/internal/HttpMessageSignature.js +320 -0
  42. package/dist/attestation/internal/HttpMessageSignature.js.map +1 -0
  43. package/dist/attestation/internal/SigningContext.d.ts +4 -0
  44. package/dist/attestation/internal/SigningContext.d.ts.map +1 -0
  45. package/dist/attestation/internal/SigningContext.js +11 -0
  46. package/dist/attestation/internal/SigningContext.js.map +1 -0
  47. package/dist/cli/account.d.ts +14 -14
  48. package/dist/cli/account.d.ts.map +1 -1
  49. package/dist/cli/cli.d.ts +6 -6
  50. package/dist/cli/cli.d.ts.map +1 -1
  51. package/dist/cli/cli.js +1 -0
  52. package/dist/cli/cli.js.map +1 -1
  53. package/dist/cli/plugins/tempo.js.map +1 -1
  54. package/dist/cli/sessions/commands.d.ts +4 -4
  55. package/dist/cli/sessions/commands.d.ts.map +1 -1
  56. package/dist/cli/sessions/commands.js.map +1 -1
  57. package/dist/cli/sessions/store.d.ts.map +1 -1
  58. package/dist/cli/sessions/store.js.map +1 -1
  59. package/dist/cli/utils.d.ts +1 -1
  60. package/dist/cli/utils.d.ts.map +1 -1
  61. package/dist/cli/utils.js.map +1 -1
  62. package/dist/cli/validate/discovery.js.map +1 -1
  63. package/dist/cli/validate/index.d.ts +3 -3
  64. package/dist/cli/validate/index.d.ts.map +1 -1
  65. package/dist/cli/validate/payment.d.ts.map +1 -1
  66. package/dist/cli/validate/payment.js +18 -4
  67. package/dist/cli/validate/payment.js.map +1 -1
  68. package/dist/client/Mppx.d.ts +3 -0
  69. package/dist/client/Mppx.d.ts.map +1 -1
  70. package/dist/client/Mppx.js +13 -5
  71. package/dist/client/Mppx.js.map +1 -1
  72. package/dist/client/Transport.d.ts +3 -5
  73. package/dist/client/Transport.d.ts.map +1 -1
  74. package/dist/client/Transport.js +1 -17
  75. package/dist/client/Transport.js.map +1 -1
  76. package/dist/client/internal/Fetch.d.ts.map +1 -1
  77. package/dist/client/internal/Fetch.js +92 -13
  78. package/dist/client/internal/Fetch.js.map +1 -1
  79. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  80. package/dist/client/internal/MethodChallenge.d.ts.map +1 -1
  81. package/dist/client/internal/MethodChallenge.js +4 -0
  82. package/dist/client/internal/MethodChallenge.js.map +1 -1
  83. package/dist/client/internal/MethodResponse.d.ts +15 -0
  84. package/dist/client/internal/MethodResponse.d.ts.map +1 -1
  85. package/dist/client/internal/MethodResponse.js +10 -0
  86. package/dist/client/internal/MethodResponse.js.map +1 -1
  87. package/dist/client/node.js.map +1 -1
  88. package/dist/discovery/OpenApi.d.ts +3 -5
  89. package/dist/discovery/OpenApi.d.ts.map +1 -1
  90. package/dist/discovery/OpenApi.js +13 -34
  91. package/dist/discovery/OpenApi.js.map +1 -1
  92. package/dist/discovery/internal/Metadata.d.ts +20 -0
  93. package/dist/discovery/internal/Metadata.d.ts.map +1 -0
  94. package/dist/discovery/internal/Metadata.js +36 -0
  95. package/dist/discovery/internal/Metadata.js.map +1 -0
  96. package/dist/evm/Methods.d.ts +11 -11
  97. package/dist/evm/Methods.d.ts.map +1 -1
  98. package/dist/evm/Types.d.ts +17 -17
  99. package/dist/evm/Types.d.ts.map +1 -1
  100. package/dist/evm/client/Charge.d.ts +11 -11
  101. package/dist/evm/client/Methods.d.ts +11 -11
  102. package/dist/evm/server/Charge.d.ts +1 -1
  103. package/dist/evm/server/Charge.d.ts.map +1 -1
  104. package/dist/evm/server/Charge.js +2 -0
  105. package/dist/evm/server/Charge.js.map +1 -1
  106. package/dist/evm/server/Methods.d.ts +11 -11
  107. package/dist/internal/HeaderCodec.d.ts +2 -2
  108. package/dist/internal/HeaderCodec.d.ts.map +1 -1
  109. package/dist/internal/types.d.ts +6 -0
  110. package/dist/internal/types.d.ts.map +1 -1
  111. package/dist/middlewares/elysia.js.map +1 -1
  112. package/dist/middlewares/express.js.map +1 -1
  113. package/dist/middlewares/hono.js.map +1 -1
  114. package/dist/middlewares/nextjs.js.map +1 -1
  115. package/dist/proxy/Proxy.d.ts +2 -0
  116. package/dist/proxy/Proxy.d.ts.map +1 -1
  117. package/dist/proxy/Proxy.js +117 -9
  118. package/dist/proxy/Proxy.js.map +1 -1
  119. package/dist/proxy/Service.d.ts +36 -1
  120. package/dist/proxy/Service.d.ts.map +1 -1
  121. package/dist/proxy/Service.js +9 -8
  122. package/dist/proxy/Service.js.map +1 -1
  123. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  124. package/dist/proxy/services/anthropic.js +1 -0
  125. package/dist/proxy/services/anthropic.js.map +1 -1
  126. package/dist/proxy/services/openai.d.ts.map +1 -1
  127. package/dist/proxy/services/openai.js +1 -0
  128. package/dist/proxy/services/openai.js.map +1 -1
  129. package/dist/proxy/services/stripe.d.ts.map +1 -1
  130. package/dist/proxy/services/stripe.js +1 -0
  131. package/dist/proxy/services/stripe.js.map +1 -1
  132. package/dist/server/Mppx.d.ts +75 -13
  133. package/dist/server/Mppx.d.ts.map +1 -1
  134. package/dist/server/Mppx.js +201 -23
  135. package/dist/server/Mppx.js.map +1 -1
  136. package/dist/server/internal/html/config.d.ts +9 -9
  137. package/dist/server/internal/html/config.d.ts.map +1 -1
  138. package/dist/server/internal/html/constants.d.ts +17 -17
  139. package/dist/server/internal/html/constants.d.ts.map +1 -1
  140. package/dist/stripe/Methods.d.ts +10 -10
  141. package/dist/stripe/Methods.d.ts.map +1 -1
  142. package/dist/stripe/client/Charge.d.ts +10 -10
  143. package/dist/stripe/client/Methods.d.ts +10 -10
  144. package/dist/stripe/internal/constants.d.ts +1 -1
  145. package/dist/stripe/internal/constants.js +1 -1
  146. package/dist/stripe/internal/types.d.ts +1 -0
  147. package/dist/stripe/internal/types.d.ts.map +1 -1
  148. package/dist/stripe/server/Charge.d.ts +11 -11
  149. package/dist/stripe/server/Charge.d.ts.map +1 -1
  150. package/dist/stripe/server/Charge.js +49 -1
  151. package/dist/stripe/server/Charge.js.map +1 -1
  152. package/dist/stripe/server/Methods.d.ts +137 -48
  153. package/dist/stripe/server/Methods.d.ts.map +1 -1
  154. package/dist/stripe/server/Methods.js +259 -4
  155. package/dist/stripe/server/Methods.js.map +1 -1
  156. package/dist/stripe/server/internal/deposit-address.d.ts +13 -0
  157. package/dist/stripe/server/internal/deposit-address.d.ts.map +1 -0
  158. package/dist/stripe/server/internal/deposit-address.js +28 -0
  159. package/dist/stripe/server/internal/deposit-address.js.map +1 -0
  160. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  161. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  162. package/dist/stripe/server/internal/html.gen.js +1 -1
  163. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  164. package/dist/stripe/server/internal/record-payment.d.ts +15 -0
  165. package/dist/stripe/server/internal/record-payment.d.ts.map +1 -0
  166. package/dist/stripe/server/internal/record-payment.js +42 -0
  167. package/dist/stripe/server/internal/record-payment.js.map +1 -0
  168. package/dist/stripe/server/internal/request.d.ts +25 -0
  169. package/dist/stripe/server/internal/request.d.ts.map +1 -0
  170. package/dist/stripe/server/internal/request.js +29 -0
  171. package/dist/stripe/server/internal/request.js.map +1 -0
  172. package/dist/tap/Constants.d.ts +28 -0
  173. package/dist/tap/Constants.d.ts.map +1 -0
  174. package/dist/tap/Constants.js +32 -0
  175. package/dist/tap/Constants.js.map +1 -0
  176. package/dist/tap/Types.d.ts +11 -0
  177. package/dist/tap/Types.d.ts.map +1 -0
  178. package/dist/tap/Types.js +2 -0
  179. package/dist/tap/Types.js.map +1 -0
  180. package/dist/tap/client/index.d.ts +26 -0
  181. package/dist/tap/client/index.d.ts.map +1 -0
  182. package/dist/tap/client/index.js +33 -0
  183. package/dist/tap/client/index.js.map +1 -0
  184. package/dist/tap/index.d.ts +5 -0
  185. package/dist/tap/index.d.ts.map +1 -0
  186. package/dist/tap/index.js +5 -0
  187. package/dist/tap/index.js.map +1 -0
  188. package/dist/tap/server/index.d.ts +20 -0
  189. package/dist/tap/server/index.d.ts.map +1 -0
  190. package/dist/tap/server/index.js +36 -0
  191. package/dist/tap/server/index.js.map +1 -0
  192. package/dist/tempo/Attribution.d.ts +1 -1
  193. package/dist/tempo/Methods.d.ts +60 -60
  194. package/dist/tempo/Methods.d.ts.map +1 -1
  195. package/dist/tempo/Proof.d.ts +16 -16
  196. package/dist/tempo/client/Charge.d.ts +19 -19
  197. package/dist/tempo/client/Charge.d.ts.map +1 -1
  198. package/dist/tempo/client/Charge.js +1 -7
  199. package/dist/tempo/client/Charge.js.map +1 -1
  200. package/dist/tempo/client/Methods.d.ts +38 -38
  201. package/dist/tempo/client/Methods.js.map +1 -1
  202. package/dist/tempo/client/Subscription.d.ts +23 -23
  203. package/dist/tempo/client/Subscription.js.map +1 -1
  204. package/dist/tempo/internal/address.d.ts +2 -1
  205. package/dist/tempo/internal/address.d.ts.map +1 -1
  206. package/dist/tempo/internal/address.js +3 -1
  207. package/dist/tempo/internal/address.js.map +1 -1
  208. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  209. package/dist/tempo/internal/auto-swap.js +4 -19
  210. package/dist/tempo/internal/auto-swap.js.map +1 -1
  211. package/dist/tempo/internal/defaults.d.ts +6 -6
  212. package/dist/tempo/internal/defaults.d.ts.map +1 -1
  213. package/dist/tempo/internal/fee-payer.d.ts +12 -12
  214. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  215. package/dist/tempo/internal/fee-payer.js.map +1 -1
  216. package/dist/tempo/internal/proof.d.ts +17 -17
  217. package/dist/tempo/internal/proof.d.ts.map +1 -1
  218. package/dist/tempo/legacy/client/Session.d.ts +20 -20
  219. package/dist/tempo/legacy/client/Session.js.map +1 -1
  220. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  221. package/dist/tempo/legacy/index.d.ts +1 -3
  222. package/dist/tempo/legacy/index.d.ts.map +1 -1
  223. package/dist/tempo/legacy/index.js +1 -3
  224. package/dist/tempo/legacy/index.js.map +1 -1
  225. package/dist/tempo/legacy/session/escrow.abi.d.ts +374 -374
  226. package/dist/tempo/legacy/session/escrow.abi.d.ts.map +1 -1
  227. package/dist/tempo/server/Charge.d.ts +34 -12
  228. package/dist/tempo/server/Charge.d.ts.map +1 -1
  229. package/dist/tempo/server/Charge.js +44 -39
  230. package/dist/tempo/server/Charge.js.map +1 -1
  231. package/dist/tempo/server/Methods.d.ts +37 -52
  232. package/dist/tempo/server/Methods.d.ts.map +1 -1
  233. package/dist/tempo/server/Methods.js +0 -14
  234. package/dist/tempo/server/Methods.js.map +1 -1
  235. package/dist/tempo/server/Relay.d.ts +14 -5
  236. package/dist/tempo/server/Relay.d.ts.map +1 -1
  237. package/dist/tempo/server/Relay.js +6 -4
  238. package/dist/tempo/server/Relay.js.map +1 -1
  239. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -1
  240. package/dist/tempo/server/SponsorBudget.js +7 -1
  241. package/dist/tempo/server/SponsorBudget.js.map +1 -1
  242. package/dist/tempo/server/Subscription.d.ts +25 -25
  243. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  244. package/dist/tempo/server/Subscription.js +2 -0
  245. package/dist/tempo/server/Subscription.js.map +1 -1
  246. package/dist/tempo/server/index.d.ts +1 -1
  247. package/dist/tempo/server/index.d.ts.map +1 -1
  248. package/dist/tempo/server/index.js +1 -1
  249. package/dist/tempo/server/index.js.map +1 -1
  250. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  251. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  252. package/dist/tempo/server/internal/html.gen.js +1 -1
  253. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  254. package/dist/tempo/server/internal/transport.d.ts +2 -0
  255. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  256. package/dist/tempo/server/internal/transport.js +9 -2
  257. package/dist/tempo/server/internal/transport.js.map +1 -1
  258. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  259. package/dist/tempo/session/client/CredentialState.d.ts +1 -1
  260. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  261. package/dist/tempo/session/client/ReceiptCoordinator.js.map +1 -1
  262. package/dist/tempo/session/client/Runtime.js.map +1 -1
  263. package/dist/tempo/session/client/Session.d.ts +19 -19
  264. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  265. package/dist/tempo/session/client/Session.js +128 -14
  266. package/dist/tempo/session/client/Session.js.map +1 -1
  267. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  268. package/dist/tempo/session/client/SessionManager.js +62 -20
  269. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  270. package/dist/tempo/session/client/Transports.d.ts +9 -1
  271. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  272. package/dist/tempo/session/client/Transports.js +36 -13
  273. package/dist/tempo/session/client/Transports.js.map +1 -1
  274. package/dist/tempo/session/precompile/Chain.d.ts +27 -0
  275. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  276. package/dist/tempo/session/precompile/Chain.js +68 -7
  277. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  278. package/dist/tempo/session/precompile/Protocol.js.map +1 -1
  279. package/dist/tempo/session/precompile/Voucher.d.ts +4 -4
  280. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  281. package/dist/tempo/session/precompile/escrow.abi.d.ts +306 -306
  282. package/dist/tempo/session/precompile/escrow.abi.d.ts.map +1 -1
  283. package/dist/tempo/session/server/ChannelStore.d.ts +9 -0
  284. package/dist/tempo/session/server/ChannelStore.d.ts.map +1 -1
  285. package/dist/tempo/session/server/ChannelStore.js +16 -8
  286. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  287. package/dist/tempo/session/server/CredentialVerification.d.ts +30 -13
  288. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  289. package/dist/tempo/session/server/CredentialVerification.js +207 -13
  290. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  291. package/dist/tempo/session/server/MeteredStream.d.ts +3 -0
  292. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  293. package/dist/tempo/session/server/MeteredStream.js +3 -1
  294. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  295. package/dist/tempo/session/server/Session.d.ts +32 -21
  296. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  297. package/dist/tempo/session/server/Session.js +116 -53
  298. package/dist/tempo/session/server/Session.js.map +1 -1
  299. package/dist/tempo/session/server/Sse.d.ts +3 -1
  300. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  301. package/dist/tempo/session/server/Sse.js +2 -1
  302. package/dist/tempo/session/server/Sse.js.map +1 -1
  303. package/dist/tempo/session/server/Transports.d.ts +1 -1
  304. package/dist/tempo/session/server/Transports.d.ts.map +1 -1
  305. package/dist/tempo/session/server/Transports.js +1 -0
  306. package/dist/tempo/session/server/Transports.js.map +1 -1
  307. package/dist/tempo/session/server/Ws.d.ts +6 -1
  308. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  309. package/dist/tempo/session/server/Ws.js +4 -0
  310. package/dist/tempo/session/server/Ws.js.map +1 -1
  311. package/dist/tempo/subscription/KeyAuthorization.d.ts +38 -866
  312. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  313. package/dist/tempo/subscription/KeyAuthorization.js +14 -3
  314. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
  315. package/dist/viem/Account.d.ts.map +1 -1
  316. package/dist/viem/Account.js.map +1 -1
  317. package/dist/viem/Client.d.ts.map +1 -1
  318. package/dist/viem/Client.js +6 -1
  319. package/dist/viem/Client.js.map +1 -1
  320. package/dist/web-bot-auth/Constants.d.ts +20 -0
  321. package/dist/web-bot-auth/Constants.d.ts.map +1 -0
  322. package/dist/web-bot-auth/Constants.js +24 -0
  323. package/dist/web-bot-auth/Constants.js.map +1 -0
  324. package/dist/web-bot-auth/Types.d.ts +10 -0
  325. package/dist/web-bot-auth/Types.d.ts.map +1 -0
  326. package/dist/web-bot-auth/Types.js +2 -0
  327. package/dist/web-bot-auth/Types.js.map +1 -0
  328. package/dist/web-bot-auth/client/index.d.ts +29 -0
  329. package/dist/web-bot-auth/client/index.d.ts.map +1 -0
  330. package/dist/web-bot-auth/client/index.js +64 -0
  331. package/dist/web-bot-auth/client/index.js.map +1 -0
  332. package/dist/web-bot-auth/index.d.ts +7 -0
  333. package/dist/web-bot-auth/index.d.ts.map +1 -0
  334. package/dist/web-bot-auth/index.js +7 -0
  335. package/dist/web-bot-auth/index.js.map +1 -0
  336. package/dist/web-bot-auth/internal/JwkThumbprint.d.ts +5 -0
  337. package/dist/web-bot-auth/internal/JwkThumbprint.d.ts.map +1 -0
  338. package/dist/web-bot-auth/internal/JwkThumbprint.js +20 -0
  339. package/dist/web-bot-auth/internal/JwkThumbprint.js.map +1 -0
  340. package/dist/web-bot-auth/internal/SignatureAgent.d.ts +3 -0
  341. package/dist/web-bot-auth/internal/SignatureAgent.d.ts.map +1 -0
  342. package/dist/web-bot-auth/internal/SignatureAgent.js +18 -0
  343. package/dist/web-bot-auth/internal/SignatureAgent.js.map +1 -0
  344. package/dist/web-bot-auth/server/index.d.ts +35 -0
  345. package/dist/web-bot-auth/server/index.d.ts.map +1 -0
  346. package/dist/web-bot-auth/server/index.js +88 -0
  347. package/dist/web-bot-auth/server/index.js.map +1 -0
  348. package/dist/x402/Assets.d.ts.map +1 -1
  349. package/dist/x402/Types.d.ts +6 -6
  350. package/dist/x402/Types.d.ts.map +1 -1
  351. package/dist/x402/server/EvmCharge.d.ts.map +1 -1
  352. package/dist/x402/server/EvmCharge.js +32 -20
  353. package/dist/x402/server/EvmCharge.js.map +1 -1
  354. package/package.json +20 -4
  355. package/src/Challenge.fuzz.test.ts +80 -47
  356. package/src/Challenge.test.ts +65 -0
  357. package/src/Challenge.ts +39 -35
  358. package/src/Method.ts +82 -68
  359. package/src/Store.test-d.ts +16 -0
  360. package/src/Store.test.ts +50 -0
  361. package/src/Store.ts +68 -0
  362. package/src/attestation/Attestation.integration.test.ts +208 -0
  363. package/src/attestation/Client.test.ts +83 -0
  364. package/src/attestation/Client.ts +34 -0
  365. package/src/attestation/Constants.ts +15 -0
  366. package/src/attestation/Server.test.ts +60 -0
  367. package/src/attestation/Server.ts +12 -0
  368. package/src/attestation/Types.test-d.ts +54 -0
  369. package/src/attestation/Types.ts +77 -0
  370. package/src/attestation/index.ts +5 -0
  371. package/src/attestation/internal/HttpMessageSignature.test.ts +101 -0
  372. package/src/attestation/internal/HttpMessageSignature.ts +429 -0
  373. package/src/attestation/internal/SigningContext.ts +13 -0
  374. package/src/bin.js +2 -0
  375. package/src/cli/cli.ts +1 -0
  376. package/src/cli/validate/payment.ts +18 -4
  377. package/src/cli/validate.test.ts +16 -0
  378. package/src/client/Mppx.test-d.ts +17 -0
  379. package/src/client/Mppx.test.ts +14 -14
  380. package/src/client/Mppx.ts +25 -4
  381. package/src/client/Transport.test.ts +12 -45
  382. package/src/client/Transport.ts +3 -24
  383. package/src/client/internal/Fetch.test.ts +77 -0
  384. package/src/client/internal/Fetch.ts +112 -14
  385. package/src/client/internal/MethodChallenge.ts +5 -0
  386. package/src/client/internal/MethodResponse.ts +28 -0
  387. package/src/discovery/OpenApi.test.ts +170 -0
  388. package/src/discovery/OpenApi.ts +16 -48
  389. package/src/discovery/internal/Metadata.ts +51 -0
  390. package/src/evm/server/Charge.ts +6 -1
  391. package/src/internal/AcceptPayment.test.ts +12 -8
  392. package/src/internal/types.ts +9 -0
  393. package/src/mcp/client/McpClient.integration.test.ts +19 -32
  394. package/src/middlewares/elysia.test.ts +2 -12
  395. package/src/middlewares/express.test.ts +2 -12
  396. package/src/middlewares/hono.test.ts +6 -9
  397. package/src/middlewares/nextjs.test.ts +2 -12
  398. package/src/proxy/Proxy.test.ts +26 -19
  399. package/src/proxy/Proxy.ts +147 -9
  400. package/src/proxy/Proxy.upstreamError.test.ts +364 -0
  401. package/src/proxy/Service.test.ts +73 -3
  402. package/src/proxy/Service.ts +50 -23
  403. package/src/proxy/services/anthropic.ts +1 -0
  404. package/src/proxy/services/hooks.test-d.ts +14 -0
  405. package/src/proxy/services/hooks.test.ts +44 -0
  406. package/src/proxy/services/openai.ts +1 -0
  407. package/src/proxy/services/stripe.ts +1 -0
  408. package/src/server/Methods.test-d.ts +88 -0
  409. package/src/server/Methods.test.ts +69 -0
  410. package/src/server/Mppx.test-d.ts +141 -7
  411. package/src/server/Mppx.test.ts +1017 -119
  412. package/src/server/Mppx.ts +350 -54
  413. package/src/stripe/internal/constants.ts +1 -1
  414. package/src/stripe/internal/types.ts +1 -0
  415. package/src/stripe/server/Charge.test-d.ts +16 -1
  416. package/src/stripe/server/Charge.test.ts +79 -0
  417. package/src/stripe/server/Charge.ts +54 -1
  418. package/src/stripe/server/Methods.test-d.ts +157 -0
  419. package/src/stripe/server/Methods.test.ts +403 -0
  420. package/src/stripe/server/Methods.ts +423 -6
  421. package/src/stripe/server/internal/deposit-address.ts +57 -0
  422. package/src/stripe/server/internal/html/node_modules/.bin/mppx +3 -3
  423. package/src/stripe/server/internal/html/package.json +1 -1
  424. package/src/stripe/server/internal/html.gen.ts +1 -1
  425. package/src/stripe/server/internal/record-payment.ts +66 -0
  426. package/src/stripe/server/internal/request.ts +49 -0
  427. package/src/tap/Constants.ts +32 -0
  428. package/src/tap/Tap.test.ts +125 -0
  429. package/src/tap/Types.ts +11 -0
  430. package/src/tap/client/index.ts +54 -0
  431. package/src/tap/index.ts +4 -0
  432. package/src/tap/server/index.ts +48 -0
  433. package/src/tempo/PublicExports.test-d.ts +3 -3
  434. package/src/tempo/client/Charge.ts +1 -19
  435. package/src/tempo/internal/address.test.ts +42 -0
  436. package/src/tempo/internal/address.ts +3 -3
  437. package/src/tempo/internal/auto-swap.test.ts +4 -4
  438. package/src/tempo/internal/auto-swap.ts +7 -50
  439. package/src/tempo/legacy/README.md +5 -5
  440. package/src/tempo/legacy/index.ts +1 -3
  441. package/src/tempo/server/AtomicStore.test-d.ts +13 -0
  442. package/src/tempo/server/Charge.test.ts +19 -84
  443. package/src/tempo/server/Charge.ts +92 -56
  444. package/src/tempo/server/Methods.ts +0 -20
  445. package/src/tempo/server/Relay.test.ts +161 -7
  446. package/src/tempo/server/Relay.ts +15 -6
  447. package/src/tempo/server/SponsorBudget.test.ts +29 -0
  448. package/src/tempo/server/SponsorBudget.ts +7 -1
  449. package/src/tempo/server/Subscription.ts +4 -1
  450. package/src/tempo/server/index.ts +1 -1
  451. package/src/tempo/server/internal/html/node_modules/.bin/mppx +3 -3
  452. package/src/tempo/server/internal/html/package.json +1 -1
  453. package/src/tempo/server/internal/html.gen.ts +1 -1
  454. package/src/tempo/server/internal/transport.test.ts +22 -3
  455. package/src/tempo/server/internal/transport.ts +15 -3
  456. package/src/tempo/session/README.md +119 -0
  457. package/src/tempo/session/client/Session.test.ts +299 -33
  458. package/src/tempo/session/client/Session.ts +152 -16
  459. package/src/tempo/session/client/SessionManager.test.ts +216 -9
  460. package/src/tempo/session/client/SessionManager.ts +63 -17
  461. package/src/tempo/session/client/Transports.test.ts +22 -0
  462. package/src/tempo/session/client/Transports.ts +58 -22
  463. package/src/tempo/session/precompile/Chain.ts +131 -11
  464. package/src/tempo/session/server/ChannelStore.ts +33 -12
  465. package/src/tempo/session/server/CredentialVerification.test.ts +4 -4
  466. package/src/tempo/session/server/CredentialVerification.ts +340 -28
  467. package/src/tempo/session/server/MeteredStream.ts +5 -1
  468. package/src/tempo/session/server/Session.test.ts +483 -36
  469. package/src/tempo/session/server/Session.ts +159 -60
  470. package/src/tempo/session/server/Sse.test.ts +77 -0
  471. package/src/tempo/session/server/Sse.ts +8 -1
  472. package/src/tempo/session/server/Transports.ts +2 -1
  473. package/src/tempo/session/server/Ws.test.ts +171 -0
  474. package/src/tempo/session/server/Ws.ts +10 -1
  475. package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
  476. package/src/tempo/subscription/KeyAuthorization.ts +14 -3
  477. package/src/viem/Client.test.ts +18 -1
  478. package/src/viem/Client.ts +7 -1
  479. package/src/web-bot-auth/Constants.ts +24 -0
  480. package/src/web-bot-auth/Types.ts +9 -0
  481. package/src/web-bot-auth/WebBotAuth.test.ts +182 -0
  482. package/src/web-bot-auth/client/index.ts +84 -0
  483. package/src/web-bot-auth/index.ts +6 -0
  484. package/src/web-bot-auth/internal/JwkThumbprint.ts +21 -0
  485. package/src/web-bot-auth/internal/SignatureAgent.ts +18 -0
  486. package/src/web-bot-auth/server/index.ts +113 -0
  487. package/src/x402/Exact.e2e.test.ts +121 -81
  488. package/src/x402/Exact.localnet.test.ts +1 -1
  489. package/src/x402/server/EvmCharge.ts +35 -19
  490. package/dist/tempo/legacy/server/Session.d.ts +0 -208
  491. package/dist/tempo/legacy/server/Session.d.ts.map +0 -1
  492. package/dist/tempo/legacy/server/Session.js +0 -698
  493. package/dist/tempo/legacy/server/Session.js.map +0 -1
  494. package/dist/tempo/legacy/server/index.d.ts +0 -5
  495. package/dist/tempo/legacy/server/index.d.ts.map +0 -1
  496. package/dist/tempo/legacy/server/index.js +0 -5
  497. package/dist/tempo/legacy/server/index.js.map +0 -1
  498. package/dist/tempo/legacy/session/ChannelStore.d.ts +0 -22
  499. package/dist/tempo/legacy/session/ChannelStore.d.ts.map +0 -1
  500. package/dist/tempo/legacy/session/ChannelStore.js +0 -6
  501. package/dist/tempo/legacy/session/ChannelStore.js.map +0 -1
  502. package/src/tempo/legacy/server/Defaults.test-d.ts +0 -9
  503. package/src/tempo/legacy/server/Session.test.ts +0 -6370
  504. package/src/tempo/legacy/server/Session.ts +0 -1024
  505. package/src/tempo/legacy/server/index.ts +0 -4
  506. package/src/tempo/legacy/session/ChannelStore.test.ts +0 -58
  507. package/src/tempo/legacy/session/ChannelStore.ts +0 -39
@@ -3,7 +3,7 @@ import * as http from 'node:http'
3
3
  import { Challenge, Credential, Errors, Method, Receipt, z } from 'mppx'
4
4
  import {
5
5
  Mppx as Mppx_client,
6
- sessionLegacy as tempo_session_client,
6
+ session as tempo_session_client,
7
7
  tempo as tempo_client,
8
8
  } from 'mppx/client'
9
9
  import { Types as evm_Types } from 'mppx/evm'
@@ -12,7 +12,6 @@ import { Header as x402_Header, Types as x402_Types, type PaymentPayload } from
12
12
  import { getTransactionReceipt } from 'viem/actions'
13
13
  import { describe, expect, test, vi } from 'vp/test'
14
14
  import * as Http from '~test/Http.js'
15
- import { deployEscrow } from '~test/tempo/legacy/session.js'
16
15
  import { accounts, asset, client } from '~test/tempo/viem.js'
17
16
 
18
17
  import type { SessionReceipt } from '../tempo/session/precompile/Protocol.js'
@@ -60,6 +59,82 @@ describe('create', () => {
60
59
  })
61
60
  })
62
61
 
62
+ describe('multi-method intent handler', () => {
63
+ test('composes all methods sharing an intent into a single handler', async () => {
64
+ const schema = {
65
+ credential: { payload: z.object({ sig: z.string() }) },
66
+ request: z.object({ amount: z.string(), currency: z.string() }),
67
+ }
68
+
69
+ const method1 = Method.toServer(Method.from({ name: 'foo', intent: 'charge', schema }), {
70
+ defaults: { currency: 'usd' },
71
+ verify: async () => ({
72
+ method: 'foo',
73
+ status: 'success' as const,
74
+ reference: '0x1',
75
+ timestamp: new Date().toISOString(),
76
+ }),
77
+ })
78
+ const method2 = Method.toServer(Method.from({ name: 'bar', intent: 'charge', schema }), {
79
+ defaults: { currency: 'eur' },
80
+ verify: async () => ({
81
+ method: 'bar',
82
+ status: 'success' as const,
83
+ reference: '0x2',
84
+ timestamp: new Date().toISOString(),
85
+ }),
86
+ })
87
+
88
+ const mppx = Mppx.create({ methods: [method1, method2], secretKey, realm })
89
+
90
+ expect(typeof mppx.charge).toBe('function')
91
+
92
+ const result = await (mppx as any).charge({ amount: '100' })(new Request('http://localhost'))
93
+ expect(result.status).toBe(402)
94
+
95
+ const wwwAuth = result.challenge.headers.get('www-authenticate')
96
+ expect(wwwAuth).toContain('method="foo"')
97
+ expect(wwwAuth).toContain('method="bar"')
98
+ })
99
+
100
+ test('single method with unique intent gets direct handler', async () => {
101
+ const schema = {
102
+ credential: { payload: z.object({ sig: z.string() }) },
103
+ request: z.object({ amount: z.string(), currency: z.string() }),
104
+ }
105
+
106
+ const method1 = Method.toServer(Method.from({ name: 'foo', intent: 'charge', schema }), {
107
+ defaults: { currency: 'usd' },
108
+ verify: async () => ({
109
+ method: 'foo',
110
+ status: 'success' as const,
111
+ reference: '0x1',
112
+ timestamp: new Date().toISOString(),
113
+ }),
114
+ })
115
+ const method2 = Method.toServer(Method.from({ name: 'bar', intent: 'session', schema }), {
116
+ defaults: { currency: 'usd' },
117
+ verify: async () => ({
118
+ method: 'bar',
119
+ status: 'success' as const,
120
+ reference: '0x2',
121
+ timestamp: new Date().toISOString(),
122
+ }),
123
+ })
124
+
125
+ const mppx = Mppx.create({ methods: [method1, method2], secretKey, realm })
126
+
127
+ const result = await (mppx as any).charge({ amount: '100', currency: 'usd' })(
128
+ new Request('http://localhost'),
129
+ )
130
+ expect(result.status).toBe(402)
131
+
132
+ const wwwAuth = result.challenge.headers.get('www-authenticate')
133
+ expect(wwwAuth).toContain('method="foo"')
134
+ expect(wwwAuth).not.toContain('method="bar"')
135
+ })
136
+ })
137
+
63
138
  describe('request handler', () => {
64
139
  test('returns 402 when no Authorization header', async () => {
65
140
  const handler = Mppx.create({ methods: [method], realm, secretKey })
@@ -1466,6 +1541,131 @@ describe('server events', () => {
1466
1541
  expect(events).toEqual(['success:tx-standalone:undefined:/standalone'])
1467
1542
  })
1468
1543
 
1544
+ test('per-method onPaymentSuccess hook fires on successful payment', async () => {
1545
+ const calls: string[] = []
1546
+ const serverMethod = Method.toServer(eventCharge, {
1547
+ onPaymentSuccess: async ({ receipt }) => {
1548
+ calls.push(receipt.reference)
1549
+ },
1550
+ async verify() {
1551
+ return receipt('tx-hook')
1552
+ },
1553
+ })
1554
+ const handler = Mppx.create({ methods: [serverMethod], realm, secretKey })
1555
+
1556
+ const challenge = await handler.charge(options())(new Request('https://example.com/resource'))
1557
+ if (challenge.status !== 402) throw new Error()
1558
+ await handler.verifyCredential(
1559
+ Credential.from({
1560
+ challenge: Challenge.fromResponse(challenge.challenge),
1561
+ payload: { token: 'valid' },
1562
+ }),
1563
+ { request: options() },
1564
+ )
1565
+
1566
+ expect(calls).toEqual(['tx-hook'])
1567
+ })
1568
+
1569
+ test('onPaymentSuccess does not fire for a different method name', async () => {
1570
+ const hookCalls: string[] = []
1571
+
1572
+ const methodWithHook = Method.toServer(eventCharge, {
1573
+ onPaymentSuccess: async ({ receipt }) => {
1574
+ hookCalls.push(receipt.reference)
1575
+ },
1576
+ async verify() {
1577
+ return receipt('tx-a')
1578
+ },
1579
+ })
1580
+ const otherName = Method.from({ name: 'other', intent: 'charge', schema: eventCharge.schema })
1581
+ const methodWithoutHook = Method.toServer(otherName, {
1582
+ async verify() {
1583
+ return receipt('tx-b')
1584
+ },
1585
+ })
1586
+
1587
+ const handler = Mppx.create({ methods: [methodWithHook, methodWithoutHook], realm, secretKey })
1588
+
1589
+ // Pay the method WITHOUT the hook
1590
+ const challenge = await handler.compose(['other/charge', options()])(
1591
+ new Request('https://example.com'),
1592
+ )
1593
+ if (challenge.status !== 402) throw new Error()
1594
+ await handler.verifyCredential(
1595
+ Credential.from({
1596
+ challenge: Challenge.fromResponse(challenge.challenge),
1597
+ payload: { token: 'valid' },
1598
+ }),
1599
+ { request: options() },
1600
+ )
1601
+
1602
+ expect(hookCalls).toEqual([])
1603
+ })
1604
+
1605
+ test('onPaymentSuccess does not fire for a different intent', async () => {
1606
+ const hookCalls: string[] = []
1607
+
1608
+ const chargeMethod = Method.toServer(eventCharge, {
1609
+ onPaymentSuccess: async ({ receipt }) => {
1610
+ hookCalls.push(receipt.reference)
1611
+ },
1612
+ async verify() {
1613
+ return receipt('tx-charge')
1614
+ },
1615
+ })
1616
+ const sessionSchema = Method.from({
1617
+ name: 'mock',
1618
+ intent: 'session',
1619
+ schema: eventCharge.schema,
1620
+ })
1621
+ const sessionMethod = Method.toServer(sessionSchema, {
1622
+ async verify() {
1623
+ return receipt('tx-session')
1624
+ },
1625
+ })
1626
+
1627
+ const handler = Mppx.create({ methods: [chargeMethod, sessionMethod], realm, secretKey })
1628
+
1629
+ // Pay the session method - charge hook should NOT fire
1630
+ const challenge = await handler.session(options())(new Request('https://example.com'))
1631
+ if (challenge.status !== 402) throw new Error()
1632
+ await handler.verifyCredential(
1633
+ Credential.from({
1634
+ challenge: Challenge.fromResponse(challenge.challenge),
1635
+ payload: { token: 'valid' },
1636
+ }),
1637
+ { request: options() },
1638
+ )
1639
+
1640
+ expect(hookCalls).toEqual([])
1641
+ })
1642
+
1643
+ test('per-method onPaymentSuccess hook errors do not propagate', async () => {
1644
+ const serverMethod = Method.toServer(eventCharge, {
1645
+ onPaymentSuccess: async () => {
1646
+ throw new Error('hook failure')
1647
+ },
1648
+ async verify() {
1649
+ return receipt('tx-error')
1650
+ },
1651
+ })
1652
+ const handler = Mppx.create({ methods: [serverMethod], realm, secretKey })
1653
+
1654
+ const first = await handler.charge(options())(new Request('https://example.com/resource'))
1655
+ expect(first.status).toBe(402)
1656
+ if (first.status !== 402) throw new Error()
1657
+
1658
+ const result = await handler.verifyCredential(
1659
+ Credential.from({
1660
+ challenge: Challenge.fromResponse(first.challenge),
1661
+ payload: { token: 'valid' },
1662
+ }),
1663
+ { request: options() },
1664
+ )
1665
+
1666
+ expect(result.reference).toBe('tx-error')
1667
+ })
1668
+
1469
1669
  test('emits payment failure from standalone verifyCredential failures', async () => {
1470
1670
  const events: string[] = []
1471
1671
  const capturedRequest = {
@@ -1901,10 +2101,10 @@ describe('compose', () => {
1901
2101
  },
1902
2102
  })
1903
2103
 
1904
- const legacySessionMethod = Method.toServer(mockSession, {
1905
- alias: 'sessionLegacy',
2104
+ const alternateSessionMethod = Method.toServer(mockSession, {
2105
+ alias: 'alternateSession',
1906
2106
  async verify() {
1907
- return mockReceipt('v1')
2107
+ return mockReceipt('alternate')
1908
2108
  },
1909
2109
  })
1910
2110
 
@@ -1947,9 +2147,9 @@ describe('compose', () => {
1947
2147
  unitType: 'request',
1948
2148
  }
1949
2149
 
1950
- const legacySessionChallengeOpts = {
2150
+ const alternateSessionChallengeOpts = {
1951
2151
  ...sessionChallengeOpts,
1952
- methodDetails: { sessionProtocol: 'v1' },
2152
+ methodDetails: { sessionProtocol: 'alternate' },
1953
2153
  }
1954
2154
 
1955
2155
  test('returns 402 with multiple WWW-Authenticate headers when no credential', async () => {
@@ -1968,165 +2168,694 @@ describe('compose', () => {
1968
2168
  expect(wwwAuth).toContain('method="beta"')
1969
2169
  })
1970
2170
 
1971
- test('broadcasts duplicate tempo/session variants in compose order', async () => {
2171
+ test('selectOffers filters by method, configured request, and incoming request', async () => {
2172
+ const challenged: string[] = []
2173
+ const input = new Request('https://example.com/resource?cards=disabled', {
2174
+ body: 'request body',
2175
+ method: 'POST',
2176
+ })
1972
2177
  const mppx = Mppx.create({
1973
- methods: [tip1034SessionMethod, legacySessionMethod],
2178
+ methods: [alphaMethod, betaMethod],
1974
2179
  realm,
1975
2180
  secretKey,
2181
+ async selectOffers(offers, { request }) {
2182
+ expect(request).not.toBe(input)
2183
+ expect(await request.text()).toBe('request body')
2184
+ expect(offers).toMatchObject([
2185
+ {
2186
+ key: 'alpha/charge',
2187
+ method: { intent: 'charge', name: 'alpha' },
2188
+ request: { amount: challengeOpts.amount },
2189
+ },
2190
+ {
2191
+ key: 'beta/charge',
2192
+ method: { intent: 'charge', name: 'beta' },
2193
+ request: { amount: '50' },
2194
+ },
2195
+ ])
2196
+
2197
+ if (new URL(request.url).searchParams.get('cards') === 'disabled')
2198
+ return offers.filter(
2199
+ (offer) => offer.method.name !== 'beta' || BigInt(offer.request.amount) >= 100n,
2200
+ )
2201
+ return offers
2202
+ },
2203
+ })
2204
+ mppx.onChallengeCreated(({ method }) => {
2205
+ challenged.push(method.name)
1976
2206
  })
1977
2207
 
1978
2208
  const result = await mppx.compose(
1979
- [mppx.tempo.session, sessionChallengeOpts],
1980
- [mppx.tempo.sessionLegacy, legacySessionChallengeOpts],
1981
- )(new Request('https://example.com/resource'))
2209
+ [alphaMethod, challengeOpts],
2210
+ [betaMethod, { ...challengeOpts, amount: '50' }],
2211
+ )(input)
1982
2212
 
1983
2213
  expect(result.status).toBe(402)
1984
2214
  if (result.status !== 402) throw new Error()
2215
+ expect(
2216
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
2217
+ ).toEqual(['alpha'])
2218
+ expect(challenged).toEqual(['alpha'])
2219
+ expect(input.bodyUsed).toBe(false)
2220
+ })
2221
+
2222
+ test('offer selectors receive deeply immutable method and request snapshots', async () => {
2223
+ const guardedSession = Method.toServer(mockSession, {
2224
+ canOffer({ request }) {
2225
+ expect(Object.isFrozen(request)).toBe(true)
2226
+ expect(Object.isFrozen(request.methodDetails)).toBe(true)
2227
+ expect(Reflect.set(request.methodDetails, 'sessionProtocol', 'changed')).toBe(false)
2228
+ return true
2229
+ },
2230
+ async verify() {
2231
+ return mockReceipt('v2')
2232
+ },
2233
+ })
2234
+ const mppx = Mppx.create({
2235
+ methods: [guardedSession],
2236
+ realm,
2237
+ secretKey,
2238
+ selectOffers(offers) {
2239
+ const offer = offers[0]!
2240
+ expect(Object.isFrozen(offers)).toBe(true)
2241
+ expect(Object.isFrozen(offer)).toBe(true)
2242
+ expect(Object.isFrozen(offer.method)).toBe(true)
2243
+ expect(Object.keys(offer.method)).toEqual(['intent', 'name'])
2244
+ expect(Object.isFrozen(offer.request)).toBe(true)
2245
+ expect(Object.isFrozen(offer.request.methodDetails)).toBe(true)
2246
+ expect(Reflect.set(offer.method, 'name', 'changed')).toBe(false)
2247
+ expect(Reflect.set(offer.request.methodDetails, 'sessionProtocol', 'changed')).toBe(false)
2248
+ return offers
2249
+ },
2250
+ })
2251
+ const handler = mppx.compose([guardedSession, sessionChallengeOpts])
1985
2252
 
1986
- const challenges = Challenge.fromResponseList(result.challenge)
1987
- expect(challenges.map((challenge) => challenge.request.methodDetails)).toEqual([
1988
- { sessionProtocol: 'v2' },
1989
- { sessionProtocol: 'v1' },
1990
- ])
2253
+ for (let call = 0; call < 2; call++) {
2254
+ const result = await handler(new Request('https://example.com/resource'))
2255
+ expect(result.status).toBe(402)
2256
+ if (result.status !== 402) throw new Error()
2257
+ const challenge = Challenge.fromResponse(result.challenge)
2258
+ expect(challenge.method).toBe('tempo')
2259
+ expect(challenge.request.methodDetails).toEqual({ sessionProtocol: 'v2' })
2260
+ }
1991
2261
  })
1992
2262
 
1993
- test('keeps duplicate tempo/session variants when Accept-Payment allows tempo/session', async () => {
2263
+ test.each(['consumed', 'locked'] as const)(
2264
+ 'offer hooks preserve request metadata when the upstream body is $state',
2265
+ async (state) => {
2266
+ const hookInputs: Request[] = []
2267
+ const guardedBeta = Method.toServer(mockChargeB, {
2268
+ async canOffer({ input }) {
2269
+ hookInputs.push(input)
2270
+ expect(await input.text()).toBe('')
2271
+ return true
2272
+ },
2273
+ async verify() {
2274
+ return mockReceipt('beta')
2275
+ },
2276
+ })
2277
+ const input = new Request('https://example.com/resource?source=upstream', {
2278
+ body: 'request body',
2279
+ headers: { 'X-Request-Id': 'request-1' },
2280
+ method: 'POST',
2281
+ })
2282
+ const reader = state === 'locked' ? input.body!.getReader() : undefined
2283
+ if (state === 'consumed') await input.text()
2284
+ const mppx = Mppx.create({
2285
+ methods: [guardedBeta],
2286
+ realm,
2287
+ secretKey,
2288
+ async selectOffers(offers, { request }) {
2289
+ hookInputs.push(request)
2290
+ expect(await request.text()).toBe('')
2291
+ return offers
2292
+ },
2293
+ })
2294
+
2295
+ try {
2296
+ const result = await mppx.compose([guardedBeta, challengeOpts])(input)
2297
+ expect(result.status).toBe(402)
2298
+ } finally {
2299
+ reader?.releaseLock()
2300
+ }
2301
+
2302
+ expect(hookInputs).toHaveLength(2)
2303
+ for (const hookInput of hookInputs) {
2304
+ expect(hookInput).not.toBe(input)
2305
+ expect(hookInput.method).toBe('POST')
2306
+ expect(hookInput.headers.get('X-Request-Id')).toBe('request-1')
2307
+ expect(new URL(hookInput.url).searchParams.get('source')).toBe('upstream')
2308
+ }
2309
+ },
2310
+ )
2311
+
2312
+ test('method canOffer runs before selectOffers with offer and upstream request context', async () => {
2313
+ const calls: string[] = []
2314
+ const guardedBeta = Method.toServer(mockChargeB, {
2315
+ async canOffer({ input, request }) {
2316
+ calls.push('canOffer')
2317
+ expect(input).not.toBe(upstreamRequest)
2318
+ expect(await input.text()).toBe('request body')
2319
+ expect(request).toEqual({
2320
+ amount: '50',
2321
+ currency: challengeOpts.currency,
2322
+ decimals: challengeOpts.decimals,
2323
+ recipient: challengeOpts.recipient,
2324
+ })
2325
+ return BigInt(request.amount) >= 100n
2326
+ },
2327
+ async verify() {
2328
+ return mockReceipt('beta')
2329
+ },
2330
+ })
2331
+ const upstreamRequest = new Request('https://example.com/resource', {
2332
+ body: 'request body',
2333
+ method: 'POST',
2334
+ })
1994
2335
  const mppx = Mppx.create({
1995
- methods: [tip1034SessionMethod, legacySessionMethod],
2336
+ methods: [alphaMethod, guardedBeta],
1996
2337
  realm,
1997
2338
  secretKey,
2339
+ selectOffers(offers) {
2340
+ calls.push('selectOffers')
2341
+ expect(offers.map((offer) => offer.method.name)).toEqual(['alpha'])
2342
+ return offers
2343
+ },
1998
2344
  })
1999
2345
 
2000
2346
  const result = await mppx.compose(
2001
- [mppx.tempo.session, sessionChallengeOpts],
2002
- [mppx.tempo.sessionLegacy, legacySessionChallengeOpts],
2003
- )(
2004
- new Request('https://example.com/resource', {
2005
- headers: { 'Accept-Payment': 'tempo/session' },
2006
- }),
2007
- )
2347
+ [alphaMethod, challengeOpts],
2348
+ [guardedBeta, { ...challengeOpts, amount: '50' }],
2349
+ )(upstreamRequest)
2008
2350
 
2009
2351
  expect(result.status).toBe(402)
2010
2352
  if (result.status !== 402) throw new Error()
2353
+ expect(
2354
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
2355
+ ).toEqual(['alpha'])
2356
+ expect(calls).toEqual(['canOffer', 'selectOffers'])
2357
+ expect(upstreamRequest.bodyUsed).toBe(false)
2358
+ })
2359
+
2360
+ test('method canOffer evaluates each configured offer without selectOffers', async () => {
2361
+ const amounts: string[] = []
2362
+ const guardedBeta = Method.toServer(mockChargeB, {
2363
+ canOffer({ request }) {
2364
+ amounts.push(request.amount)
2365
+ return BigInt(request.amount) >= 100n
2366
+ },
2367
+ async verify() {
2368
+ return mockReceipt('beta')
2369
+ },
2370
+ })
2371
+ const mppx = Mppx.create({ methods: [guardedBeta], realm, secretKey })
2011
2372
 
2012
- expect(Challenge.fromResponseList(result.challenge)).toHaveLength(2)
2373
+ const handler = mppx.compose(
2374
+ [guardedBeta, { ...challengeOpts, amount: '50' }],
2375
+ [guardedBeta, { ...challengeOpts, amount: '100' }],
2376
+ )
2377
+ expect(handler._internal?.offers).toHaveLength(2)
2378
+
2379
+ const result = await handler(new Request('https://example.com/resource'))
2380
+
2381
+ expect(result.status).toBe(402)
2382
+ if (result.status !== 402) throw new Error()
2383
+ expect(amounts).toEqual(['50', '100'])
2384
+ expect(
2385
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.request.amount),
2386
+ ).toEqual(['100'])
2013
2387
  })
2014
2388
 
2015
- test('uses TIP-1034 as the bare tempo/session handler regardless of registration order', async () => {
2016
- const mppx = Mppx.create({
2017
- methods: [legacySessionMethod, tip1034SessionMethod],
2018
- realm,
2019
- secretKey,
2389
+ test('static compose applies method canOffer', async () => {
2390
+ const guardedBeta = Method.toServer(mockChargeB, {
2391
+ canOffer: () => false,
2392
+ async verify() {
2393
+ return mockReceipt('beta')
2394
+ },
2020
2395
  })
2396
+ const mppx = Mppx.create({ methods: [alphaMethod, guardedBeta], realm, secretKey })
2021
2397
 
2022
- const result = await mppx.compose([mppx.tempo.session, sessionChallengeOpts])(
2023
- new Request('https://example.com/resource'),
2024
- )
2398
+ const result = await Mppx.compose(
2399
+ mppx.alpha.charge(challengeOpts),
2400
+ mppx.beta.charge(challengeOpts),
2401
+ )(new Request('https://example.com/resource'))
2025
2402
 
2026
2403
  expect(result.status).toBe(402)
2027
2404
  if (result.status !== 402) throw new Error()
2028
- expect(Challenge.fromResponse(result.challenge).request.methodDetails).toEqual({
2029
- sessionProtocol: 'v2',
2030
- })
2405
+ expect(
2406
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
2407
+ ).toEqual(['alpha'])
2031
2408
  })
2032
2409
 
2033
- test('rejects cross-protocol tempo/session credential replay', async () => {
2034
- const mppx = Mppx.create({
2035
- methods: [tip1034SessionMethod, legacySessionMethod],
2036
- realm,
2037
- secretKey,
2410
+ test('method canOffer does not gate a direct method handler', async () => {
2411
+ const canOffer = vi.fn(() => false)
2412
+ const guardedAlpha = Method.toServer(mockChargeA, {
2413
+ canOffer,
2414
+ async verify() {
2415
+ return mockReceipt('alpha')
2416
+ },
2038
2417
  })
2039
- const tip1034ChallengeResponse = await mppx.compose([mppx.tempo.session, sessionChallengeOpts])(
2040
- new Request('https://example.com/resource'),
2041
- )
2042
- expect(tip1034ChallengeResponse.status).toBe(402)
2043
- if (tip1034ChallengeResponse.status !== 402) throw new Error()
2044
- const tip1034Challenge = Challenge.fromResponse(tip1034ChallengeResponse.challenge)
2418
+ const mppx = Mppx.create({ methods: [guardedAlpha], realm, secretKey })
2045
2419
 
2046
- const credential = Credential.serialize({
2047
- challenge: tip1034Challenge,
2048
- payload: { token: 'valid' },
2049
- })
2050
- const legacyResult = await mppx.compose([mppx.tempo.sessionLegacy, legacySessionChallengeOpts])(
2051
- new Request('https://example.com/resource', {
2052
- headers: { Authorization: credential },
2053
- }),
2420
+ const result = await mppx.alpha.charge(challengeOpts)(
2421
+ new Request('https://example.com/resource'),
2054
2422
  )
2055
2423
 
2056
- expect(legacyResult.status).toBe(402)
2424
+ expect(result.status).toBe(402)
2425
+ expect(canOffer).not.toHaveBeenCalled()
2057
2426
  })
2058
2427
 
2059
- test('verifyCredential dispatches duplicate tempo/session variants by sessionProtocol', async () => {
2060
- const mppx = Mppx.create({
2061
- methods: [tip1034SessionMethod, legacySessionMethod],
2062
- realm,
2063
- secretKey,
2428
+ test('method canOffer filters before preflight and authorize hooks', async () => {
2429
+ const authorize = vi.fn()
2430
+ const preflight = vi.fn()
2431
+ const guardedBeta = Method.toServer(mockChargeB, {
2432
+ async authorize() {
2433
+ authorize()
2434
+ return undefined
2435
+ },
2436
+ canOffer: () => false,
2437
+ async preflight() {
2438
+ preflight()
2439
+ return undefined
2440
+ },
2441
+ async verify() {
2442
+ return mockReceipt('beta')
2443
+ },
2064
2444
  })
2065
- const tip1034Challenge = await mppx.challenge.tempo.session(sessionChallengeOpts)
2066
- const legacyChallenge = await mppx.challenge.tempo.sessionLegacy(legacySessionChallengeOpts)
2445
+ const mppx = Mppx.create({ methods: [alphaMethod, guardedBeta], realm, secretKey })
2067
2446
 
2068
- const tip1034Receipt = await mppx.verifyCredential(
2069
- Credential.serialize({
2070
- challenge: tip1034Challenge,
2071
- payload: { token: 'valid' },
2072
- }),
2073
- )
2074
- const legacyReceipt = await mppx.verifyCredential(
2075
- Credential.serialize({
2076
- challenge: legacyChallenge,
2077
- payload: { token: 'valid' },
2078
- }),
2079
- )
2447
+ const result = await mppx.compose(
2448
+ [alphaMethod, challengeOpts],
2449
+ [guardedBeta, challengeOpts],
2450
+ )(new Request('https://example.com/resource'))
2080
2451
 
2081
- expect(tip1034Receipt.reference).toBe('tx-v2')
2082
- expect(legacyReceipt.reference).toBe('tx-v1')
2452
+ expect(result.status).toBe(402)
2453
+ expect(preflight).not.toHaveBeenCalled()
2454
+ expect(authorize).not.toHaveBeenCalled()
2083
2455
  })
2084
2456
 
2085
- test('verifyCredential routes unmarked tempo/session challenges to legacy v1', async () => {
2086
- const mppx = Mppx.create({
2087
- methods: [legacySessionMethod, tip1034SessionMethod],
2088
- realm,
2089
- secretKey,
2090
- })
2091
- const unmarkedLegacyChallenge = await mppx.challenge.tempo.sessionLegacy({
2092
- ...legacySessionChallengeOpts,
2093
- methodDetails: {},
2457
+ test.each([
2458
+ {
2459
+ canOffer: () => 'yes',
2460
+ expected: 'canOffer() must return a boolean',
2461
+ name: 'non-boolean result',
2462
+ },
2463
+ {
2464
+ canOffer: () => false,
2465
+ expected: 'No payment offers are available for this request',
2466
+ name: 'no eligible offers',
2467
+ },
2468
+ ])('rejects invalid method offer availability: $name', async ({ canOffer, expected }) => {
2469
+ const guardedAlpha = Method.toServer(mockChargeA, {
2470
+ canOffer: canOffer as never,
2471
+ async verify() {
2472
+ return mockReceipt('alpha')
2473
+ },
2094
2474
  })
2475
+ const mppx = Mppx.create({ methods: [guardedAlpha], realm, secretKey })
2095
2476
 
2096
2477
  await expect(
2097
- mppx.verifyCredential(
2098
- Credential.serialize({
2099
- challenge: unmarkedLegacyChallenge,
2100
- payload: { token: 'valid' },
2101
- }),
2102
- ),
2103
- ).resolves.toMatchObject({ reference: 'tx-v1' })
2478
+ mppx.compose([guardedAlpha, challengeOpts])(new Request('https://example.com/resource')),
2479
+ ).rejects.toThrow(expected)
2104
2480
  })
2105
2481
 
2106
- test('verifyCredential rejects unknown tempo/session protocol markers', async () => {
2107
- const mppx = Mppx.create({
2108
- methods: [legacySessionMethod, tip1034SessionMethod],
2109
- realm,
2110
- secretKey,
2111
- })
2112
- const challenge = await mppx.challenge.tempo.session({
2113
- ...sessionChallengeOpts,
2114
- methodDetails: { sessionProtocol: 'future' },
2482
+ test('propagates method canOffer rejections', async () => {
2483
+ const error = new Error('method offer selection failed')
2484
+ const guardedAlpha = Method.toServer(mockChargeA, {
2485
+ async canOffer() {
2486
+ throw error
2487
+ },
2488
+ async verify() {
2489
+ return mockReceipt('alpha')
2490
+ },
2115
2491
  })
2492
+ const mppx = Mppx.create({ methods: [guardedAlpha], realm, secretKey })
2116
2493
 
2117
2494
  await expect(
2118
- mppx.verifyCredential(Credential.serialize({ challenge, payload: { token: 'valid' } })),
2119
- ).rejects.toThrow('no registered method for tempo/session')
2495
+ mppx.compose([guardedAlpha, challengeOpts])(new Request('https://example.com/resource')),
2496
+ ).rejects.toBe(error)
2120
2497
  })
2121
2498
 
2122
- test('returns composed x402 challenge headers when no credential', async () => {
2123
- const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
2499
+ test('method canOffer does not prevent redemption of an issued offer', async () => {
2500
+ const canOffer = vi.fn(() => false)
2501
+ const guardedBeta = Method.toServer(mockChargeB, {
2502
+ canOffer,
2503
+ async verify() {
2504
+ return mockReceipt('beta')
2505
+ },
2506
+ })
2507
+ const mppx = Mppx.create({ methods: [guardedBeta], realm, secretKey })
2508
+ const challenge = await mppx.challenge.beta.charge(challengeOpts)
2509
+ const credential = Credential.from({ challenge, payload: { token: 'valid' } })
2124
2510
 
2125
- const result = await mppx.compose(['evm/charge', { amount: '0.01' }])(
2126
- new Request('https://example.com/resource'),
2511
+ const result = await mppx.compose([guardedBeta, challengeOpts])(
2512
+ new Request('https://example.com/resource', {
2513
+ headers: { Authorization: Credential.serialize(credential) },
2514
+ }),
2127
2515
  )
2128
2516
 
2129
- expect(result.status).toBe(402)
2517
+ expect(result.status).toBe(200)
2518
+ expect(canOffer).not.toHaveBeenCalled()
2519
+ })
2520
+
2521
+ test('selectOffers does not prevent redemption of a previously issued offer', async () => {
2522
+ let selectionCount = 0
2523
+ const mppx = Mppx.create({
2524
+ methods: [alphaMethod, betaMethod],
2525
+ realm,
2526
+ secretKey,
2527
+ selectOffers(offers, { request }) {
2528
+ selectionCount++
2529
+ return request.headers.has('X-Hide-Beta')
2530
+ ? offers.filter((offer) => offer.method.name !== 'beta')
2531
+ : offers
2532
+ },
2533
+ })
2534
+ const handler = mppx.compose([alphaMethod, challengeOpts], [betaMethod, challengeOpts])
2535
+ const challengeResult = await handler(new Request('https://example.com/resource'))
2536
+ expect(challengeResult.status).toBe(402)
2537
+ if (challengeResult.status !== 402) throw new Error()
2538
+ const betaChallenge = Challenge.fromResponseList(challengeResult.challenge).find(
2539
+ (challenge) => challenge.method === 'beta',
2540
+ )!
2541
+ const credential = Credential.from({
2542
+ challenge: betaChallenge,
2543
+ payload: { token: 'valid' },
2544
+ })
2545
+
2546
+ const result = await handler(
2547
+ new Request('https://example.com/resource', {
2548
+ headers: {
2549
+ Authorization: Credential.serialize(credential),
2550
+ 'X-Hide-Beta': 'true',
2551
+ },
2552
+ }),
2553
+ )
2554
+
2555
+ expect(result.status).toBe(200)
2556
+ expect(selectionCount).toBe(1)
2557
+ })
2558
+
2559
+ test.each(['malformed', 'unmatched'] as const)(
2560
+ 'applies selectOffers before rejecting a $kind payment credential',
2561
+ async (kind) => {
2562
+ let selectionCount = 0
2563
+ const mppx = Mppx.create({
2564
+ methods: [alphaMethod, betaMethod],
2565
+ realm,
2566
+ secretKey,
2567
+ selectOffers(offers) {
2568
+ selectionCount++
2569
+ return offers.filter((offer) => offer.method.name === 'beta')
2570
+ },
2571
+ })
2572
+ const authorization =
2573
+ kind === 'malformed'
2574
+ ? 'Payment invalid'
2575
+ : Credential.serialize(
2576
+ Credential.from({
2577
+ challenge: {
2578
+ ...(await mppx.challenge.alpha.charge(challengeOpts)),
2579
+ method: 'unknown',
2580
+ },
2581
+ payload: { token: 'invalid' },
2582
+ }),
2583
+ )
2584
+
2585
+ const result = await mppx.compose(
2586
+ [alphaMethod, challengeOpts],
2587
+ [betaMethod, challengeOpts],
2588
+ )(
2589
+ new Request('https://example.com/resource', {
2590
+ headers: { Authorization: authorization },
2591
+ }),
2592
+ )
2593
+
2594
+ expect(result.status).toBe(402)
2595
+ if (result.status !== 402) throw new Error()
2596
+ expect(Challenge.fromResponse(result.challenge).method).toBe('beta')
2597
+ expect(selectionCount).toBe(1)
2598
+ },
2599
+ )
2600
+
2601
+ test.each([
2602
+ {
2603
+ expected: 'return an array',
2604
+ name: 'non-array result',
2605
+ select: () => null,
2606
+ },
2607
+ {
2608
+ expected: 'select at least one offer',
2609
+ name: 'empty selection',
2610
+ select: (offers: readonly unknown[]) => offers.slice(0, 0),
2611
+ },
2612
+ {
2613
+ expected: 'only offers from its input array',
2614
+ name: 'copied offer',
2615
+ select: (offers: readonly unknown[]) => [{ ...(offers[0] as object) }],
2616
+ },
2617
+ {
2618
+ expected: 'preserve offer order',
2619
+ name: 'duplicate offer',
2620
+ select: (offers: readonly unknown[]) => [offers[0], offers[0]],
2621
+ },
2622
+ {
2623
+ expected: 'preserve offer order',
2624
+ name: 'reordered offers',
2625
+ select: (offers: readonly unknown[]) => [offers[1], offers[0]],
2626
+ },
2627
+ ])('rejects invalid selectOffers result: $name', async ({ expected, select }) => {
2628
+ const mppx = Mppx.create({
2629
+ methods: [alphaMethod, betaMethod],
2630
+ realm,
2631
+ secretKey,
2632
+ selectOffers: select as never,
2633
+ })
2634
+ const handler = mppx.compose([alphaMethod, challengeOpts], [betaMethod, challengeOpts])
2635
+
2636
+ await expect(handler(new Request('https://example.com/resource'))).rejects.toThrow(expected)
2637
+ })
2638
+
2639
+ test('retains all configured offers as discovery metadata when selectOffers is dynamic', () => {
2640
+ const mppx = Mppx.create({
2641
+ methods: [alphaMethod, betaMethod],
2642
+ realm,
2643
+ secretKey,
2644
+ selectOffers: (offers) => offers.slice(0, 1),
2645
+ })
2646
+ const configured = mppx.compose([alphaMethod, challengeOpts], [betaMethod, challengeOpts])
2647
+
2648
+ expect(configured._internal?.offers).toMatchObject([
2649
+ {
2650
+ _canonicalRequest: {
2651
+ amount: challengeOpts.amount,
2652
+ currency: challengeOpts.currency,
2653
+ recipient: challengeOpts.recipient,
2654
+ },
2655
+ intent: 'charge',
2656
+ name: 'alpha',
2657
+ },
2658
+ {
2659
+ _canonicalRequest: {
2660
+ amount: challengeOpts.amount,
2661
+ currency: challengeOpts.currency,
2662
+ recipient: challengeOpts.recipient,
2663
+ },
2664
+ intent: 'charge',
2665
+ name: 'beta',
2666
+ },
2667
+ ])
2668
+
2669
+ const custom = async () =>
2670
+ ({
2671
+ status: 402,
2672
+ challenge: new Response(null, { status: 402 }),
2673
+ }) as const
2674
+ const composed = Mppx.compose(mppx.alpha.charge(challengeOpts), custom)
2675
+ expect(composed._internal?.offers).toMatchObject([{ intent: 'charge', name: 'alpha' }])
2676
+ })
2677
+
2678
+ test('does not run preflight or authorize hooks for filtered offers', async () => {
2679
+ const authorize = vi.fn()
2680
+ const preflight = vi.fn()
2681
+ const filteredMethod = Method.toServer(mockChargeB, {
2682
+ async authorize() {
2683
+ authorize()
2684
+ return undefined
2685
+ },
2686
+ async preflight() {
2687
+ preflight()
2688
+ return undefined
2689
+ },
2690
+ async verify() {
2691
+ return mockReceipt('beta')
2692
+ },
2693
+ })
2694
+ const mppx = Mppx.create({
2695
+ methods: [alphaMethod, filteredMethod],
2696
+ realm,
2697
+ secretKey,
2698
+ selectOffers: (offers) => offers.filter((offer) => offer.method.name !== 'beta'),
2699
+ })
2700
+
2701
+ const result = await mppx.compose(
2702
+ [alphaMethod, challengeOpts],
2703
+ [filteredMethod, challengeOpts],
2704
+ )(new Request('https://example.com/resource'))
2705
+
2706
+ expect(result.status).toBe(402)
2707
+ expect(preflight).not.toHaveBeenCalled()
2708
+ expect(authorize).not.toHaveBeenCalled()
2709
+ })
2710
+
2711
+ test('selectOffers distinguishes aliased methods that share a canonical key', async () => {
2712
+ const mppx = Mppx.create({
2713
+ methods: [tip1034SessionMethod, alternateSessionMethod],
2714
+ realm,
2715
+ secretKey,
2716
+ selectOffers(offers) {
2717
+ expect(offers.map((offer) => offer.key)).toEqual(['tempo/session', 'tempo/session'])
2718
+ expect(offers.map((offer) => offer.method.alias)).toEqual([undefined, 'alternateSession'])
2719
+ expect(offers.map((offer) => offer.request.methodDetails)).toEqual([
2720
+ { sessionProtocol: 'v2' },
2721
+ { sessionProtocol: 'alternate' },
2722
+ ])
2723
+ return offers.filter((offer) => offer.method.alias === 'alternateSession')
2724
+ },
2725
+ })
2726
+
2727
+ const result = await mppx.compose(
2728
+ [mppx.tempo.session, sessionChallengeOpts],
2729
+ [mppx.tempo.alternateSession, alternateSessionChallengeOpts],
2730
+ )(new Request('https://example.com/resource'))
2731
+
2732
+ expect(result.status).toBe(402)
2733
+ if (result.status !== 402) throw new Error()
2734
+ expect(Challenge.fromResponse(result.challenge).request.methodDetails).toEqual({
2735
+ sessionProtocol: 'alternate',
2736
+ })
2737
+ })
2738
+
2739
+ test('selectOffers receives schema-transformed request values', async () => {
2740
+ const transformedCharge = Method.from({
2741
+ name: 'transformed',
2742
+ intent: 'charge',
2743
+ schema: {
2744
+ credential: {
2745
+ payload: z.object({ token: z.string() }),
2746
+ },
2747
+ request: z.pipe(
2748
+ z.object({
2749
+ amount: z.string(),
2750
+ currency: z.string(),
2751
+ decimals: z.number(),
2752
+ recipient: z.string(),
2753
+ }),
2754
+ z.transform(({ amount, currency, decimals, recipient }) => ({
2755
+ amount: String(Number(amount) * 10 ** decimals),
2756
+ currency,
2757
+ methodDetails: { decimals },
2758
+ recipient,
2759
+ })),
2760
+ ),
2761
+ },
2762
+ })
2763
+ const transformedMethod = Method.toServer(transformedCharge, {
2764
+ async verify() {
2765
+ return mockReceipt('transformed')
2766
+ },
2767
+ })
2768
+ const mppx = Mppx.create({
2769
+ methods: [transformedMethod],
2770
+ realm,
2771
+ secretKey,
2772
+ selectOffers(offers) {
2773
+ expect(offers[0]?.request).toEqual({
2774
+ amount: '125',
2775
+ currency: challengeOpts.currency,
2776
+ methodDetails: { decimals: 2 },
2777
+ recipient: challengeOpts.recipient,
2778
+ })
2779
+ return offers
2780
+ },
2781
+ })
2782
+
2783
+ const result = await mppx.compose([
2784
+ transformedMethod,
2785
+ { ...challengeOpts, amount: '1.25', decimals: 2 },
2786
+ ])(new Request('https://example.com/resource'))
2787
+
2788
+ expect(result.status).toBe(402)
2789
+ })
2790
+
2791
+ test('propagates selectOffers rejections', async () => {
2792
+ const error = new Error('offer selection failed')
2793
+ const mppx = Mppx.create({
2794
+ methods: [alphaMethod],
2795
+ realm,
2796
+ secretKey,
2797
+ async selectOffers() {
2798
+ throw error
2799
+ },
2800
+ })
2801
+
2802
+ await expect(
2803
+ mppx.compose([alphaMethod, challengeOpts])(new Request('https://example.com/resource')),
2804
+ ).rejects.toBe(error)
2805
+ })
2806
+
2807
+ test('broadcasts duplicate tempo/session variants in compose order', async () => {
2808
+ const mppx = Mppx.create({
2809
+ methods: [tip1034SessionMethod, alternateSessionMethod],
2810
+ realm,
2811
+ secretKey,
2812
+ })
2813
+
2814
+ const result = await mppx.compose(
2815
+ [mppx.tempo.session, sessionChallengeOpts],
2816
+ [mppx.tempo.alternateSession, alternateSessionChallengeOpts],
2817
+ )(new Request('https://example.com/resource'))
2818
+
2819
+ expect(result.status).toBe(402)
2820
+ if (result.status !== 402) throw new Error()
2821
+
2822
+ const challenges = Challenge.fromResponseList(result.challenge)
2823
+ expect(challenges.map((challenge) => challenge.request.methodDetails)).toEqual([
2824
+ { sessionProtocol: 'v2' },
2825
+ { sessionProtocol: 'alternate' },
2826
+ ])
2827
+ })
2828
+
2829
+ test('keeps duplicate tempo/session variants when Accept-Payment allows tempo/session', async () => {
2830
+ const mppx = Mppx.create({
2831
+ methods: [tip1034SessionMethod, alternateSessionMethod],
2832
+ realm,
2833
+ secretKey,
2834
+ })
2835
+
2836
+ const result = await mppx.compose(
2837
+ [mppx.tempo.session, sessionChallengeOpts],
2838
+ [mppx.tempo.alternateSession, alternateSessionChallengeOpts],
2839
+ )(
2840
+ new Request('https://example.com/resource', {
2841
+ headers: { 'Accept-Payment': 'tempo/session' },
2842
+ }),
2843
+ )
2844
+
2845
+ expect(result.status).toBe(402)
2846
+ if (result.status !== 402) throw new Error()
2847
+
2848
+ expect(Challenge.fromResponseList(result.challenge)).toHaveLength(2)
2849
+ })
2850
+
2851
+ test('returns composed x402 challenge headers when no credential', async () => {
2852
+ const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
2853
+
2854
+ const result = await mppx.compose(['evm/charge', { amount: '0.01' }])(
2855
+ new Request('https://example.com/resource'),
2856
+ )
2857
+
2858
+ expect(result.status).toBe(402)
2130
2859
  if (result.status !== 402) throw new Error()
2131
2860
 
2132
2861
  expect(result.challenge.headers.get('WWW-Authenticate')).toContain('method="evm"')
@@ -2164,6 +2893,44 @@ describe('compose', () => {
2164
2893
  expect(paymentRequired.accepts.map((accepted) => accepted.amount)).toEqual(['10000'])
2165
2894
  })
2166
2895
 
2896
+ test('selectOffers filters x402 challenge headers', async () => {
2897
+ const mppx = Mppx.create({
2898
+ methods: [alphaMethod, x402Method],
2899
+ realm,
2900
+ secretKey,
2901
+ selectOffers: (offers) => offers.filter((offer) => offer.method.name !== 'evm'),
2902
+ })
2903
+
2904
+ const result = await mppx.compose(
2905
+ [alphaMethod, challengeOpts],
2906
+ ['evm/charge', { amount: '0.01' }],
2907
+ )(new Request('https://example.com/resource'))
2908
+
2909
+ expect(result.status).toBe(402)
2910
+ if (result.status !== 402) throw new Error()
2911
+ expect(
2912
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
2913
+ ).toEqual(['alpha'])
2914
+ expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeNull()
2915
+ })
2916
+
2917
+ test('method canOffer filters x402 challenge headers', async () => {
2918
+ const guardedX402 = { ...x402Method, canOffer: () => false }
2919
+ const mppx = Mppx.create({ methods: [alphaMethod, guardedX402], realm, secretKey })
2920
+
2921
+ const result = await mppx.compose(
2922
+ [alphaMethod, challengeOpts],
2923
+ [guardedX402, { amount: '0.01' }],
2924
+ )(new Request('https://example.com/resource'))
2925
+
2926
+ expect(result.status).toBe(402)
2927
+ if (result.status !== 402) throw new Error()
2928
+ expect(
2929
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
2930
+ ).toEqual(['alpha'])
2931
+ expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeNull()
2932
+ })
2933
+
2167
2934
  test('keeps pure x402 challenge headers when Payment auth challenges are present', async () => {
2168
2935
  const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
2169
2936
  const pureX402 = async () =>
@@ -2275,6 +3042,55 @@ describe('compose', () => {
2275
3042
  expect(challenges[0]?.method).toBe('beta')
2276
3043
  })
2277
3044
 
3045
+ test('applies selectOffers before Accept-Payment negotiation', async () => {
3046
+ const mppx = Mppx.create({
3047
+ methods: [alphaMethod, betaMethod],
3048
+ realm,
3049
+ secretKey,
3050
+ selectOffers: (offers) => offers.filter((offer) => offer.method.name !== 'beta'),
3051
+ })
3052
+
3053
+ const result = await mppx.compose(
3054
+ [alphaMethod, challengeOpts],
3055
+ [betaMethod, challengeOpts],
3056
+ )(
3057
+ new Request('https://example.com/resource', {
3058
+ headers: { 'Accept-Payment': 'beta/charge' },
3059
+ }),
3060
+ )
3061
+
3062
+ expect(result.status).toBe(402)
3063
+ if (result.status !== 402) throw new Error()
3064
+ expect(
3065
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
3066
+ ).toEqual(['alpha'])
3067
+ })
3068
+
3069
+ test('applies method canOffer before Accept-Payment negotiation', async () => {
3070
+ const guardedBeta = Method.toServer(mockChargeB, {
3071
+ canOffer: () => false,
3072
+ async verify() {
3073
+ return mockReceipt('beta')
3074
+ },
3075
+ })
3076
+ const mppx = Mppx.create({ methods: [alphaMethod, guardedBeta], realm, secretKey })
3077
+
3078
+ const result = await mppx.compose(
3079
+ [alphaMethod, challengeOpts],
3080
+ [guardedBeta, challengeOpts],
3081
+ )(
3082
+ new Request('https://example.com/resource', {
3083
+ headers: { 'Accept-Payment': 'beta/charge' },
3084
+ }),
3085
+ )
3086
+
3087
+ expect(result.status).toBe(402)
3088
+ if (result.status !== 402) throw new Error()
3089
+ expect(
3090
+ Challenge.fromResponseList(result.challenge).map((challenge) => challenge.method),
3091
+ ).toEqual(['alpha'])
3092
+ })
3093
+
2278
3094
  test('filters compose x402 challenge headers using Accept-Payment', async () => {
2279
3095
  const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
2280
3096
 
@@ -2429,6 +3245,34 @@ describe('compose', () => {
2429
3245
  expect(result.status).toBe(200)
2430
3246
  })
2431
3247
 
3248
+ test('dispatches credentials to a nested composed handler', async () => {
3249
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
3250
+ const handle = Mppx.compose(
3251
+ mppx.alpha.charge(challengeOpts),
3252
+ Mppx.compose(mppx.beta.charge(challengeOpts)),
3253
+ )
3254
+
3255
+ const firstResult = await handle(new Request('https://example.com/resource'))
3256
+ expect(firstResult.status).toBe(402)
3257
+ if (firstResult.status !== 402) throw new Error()
3258
+
3259
+ const betaChallenge = Challenge.fromResponseList(firstResult.challenge).find(
3260
+ (challenge) => challenge.method === 'beta',
3261
+ )!
3262
+ const credential = Credential.from({
3263
+ challenge: betaChallenge,
3264
+ payload: { token: 'valid' },
3265
+ })
3266
+
3267
+ const result = await handle(
3268
+ new Request('https://example.com/resource', {
3269
+ headers: { Authorization: Credential.serialize(credential) },
3270
+ }),
3271
+ )
3272
+
3273
+ expect(result.status).toBe(200)
3274
+ })
3275
+
2432
3276
  test('returns 402 when credential method does not match any handler', async () => {
2433
3277
  const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
2434
3278
 
@@ -2797,6 +3641,64 @@ describe('compose', () => {
2797
3641
  expect(dataValues[1]!.config).toEqual({ providerB: true })
2798
3642
  })
2799
3643
 
3644
+ test('selectOffers filters html payment options', async () => {
3645
+ const mppx = Mppx.create({
3646
+ methods: [alphaWithHtml, betaWithHtml],
3647
+ realm,
3648
+ secretKey,
3649
+ selectOffers: (offers) => offers.filter((offer) => offer.method.name !== 'beta'),
3650
+ })
3651
+
3652
+ const result = await mppx.compose(
3653
+ [alphaWithHtml, challengeOpts],
3654
+ [betaWithHtml, challengeOpts],
3655
+ )(
3656
+ new Request('https://example.com/resource', {
3657
+ headers: { Accept: 'text/html' },
3658
+ }),
3659
+ )
3660
+
3661
+ expect(result.status).toBe(402)
3662
+ if (result.status !== 402) throw new Error()
3663
+ const body = await result.challenge.text()
3664
+ expect(body).toContain('/alpha-bundle.js')
3665
+ expect(body).not.toContain('/beta-bundle.js')
3666
+ expect(body).not.toContain('role="tablist"')
3667
+
3668
+ const dataMatch = body.match(
3669
+ /<script[^>]*id="__MPPX_DATA__"[^>]*type="application\/json"[^>]*>\s*([\s\S]*?)\s*<\/script>/s,
3670
+ )
3671
+ const dataMap = JSON.parse(dataMatch![1]!.replace(/\\u003c/g, '<'))
3672
+ expect(Object.values(dataMap)).toMatchObject([{ label: 'alpha' }])
3673
+ })
3674
+
3675
+ test('method canOffer filters html payment options', async () => {
3676
+ const guardedBeta = Method.toServer(mockChargeB, {
3677
+ canOffer: () => false,
3678
+ html: htmlOptionsB,
3679
+ async verify() {
3680
+ return mockReceipt('beta')
3681
+ },
3682
+ })
3683
+ const mppx = Mppx.create({ methods: [alphaWithHtml, guardedBeta], realm, secretKey })
3684
+
3685
+ const result = await mppx.compose(
3686
+ [alphaWithHtml, challengeOpts],
3687
+ [guardedBeta, challengeOpts],
3688
+ )(
3689
+ new Request('https://example.com/resource', {
3690
+ headers: { Accept: 'text/html' },
3691
+ }),
3692
+ )
3693
+
3694
+ expect(result.status).toBe(402)
3695
+ if (result.status !== 402) throw new Error()
3696
+ const body = await result.challenge.text()
3697
+ expect(body).toContain('/alpha-bundle.js')
3698
+ expect(body).not.toContain('/beta-bundle.js')
3699
+ expect(body).not.toContain('role="tablist"')
3700
+ })
3701
+
2800
3702
  test('returns html without tabs when single method has html', async () => {
2801
3703
  const mppx = Mppx.create({
2802
3704
  methods: [alphaWithHtml, betaMethod],
@@ -5594,15 +6496,13 @@ describe('verifyCredential', () => {
5594
6496
  })
5595
6497
 
5596
6498
  test('verifies real session open and voucher credentials created from 402 responses', async () => {
5597
- const escrowContract = await deployEscrow()
5598
6499
  const server = Mppx.create({
5599
6500
  methods: [
5600
- tempo.sessionLegacy({
6501
+ tempo.session({
5601
6502
  store: Store.memory(),
5602
6503
  getClient: () => client,
5603
6504
  account: accounts[0],
5604
6505
  currency: asset,
5605
- escrowContract,
5606
6506
  chainId: client.chain!.id,
5607
6507
  }),
5608
6508
  ],
@@ -5622,7 +6522,7 @@ describe('verifyCredential', () => {
5622
6522
 
5623
6523
  const httpServer = await Http.createServer(async (req, res) => {
5624
6524
  const result = await Mppx.toNodeListener(
5625
- server.session({ amount: '1', unitType: 'request' }),
6525
+ server.session({ amount: '1', suggestedDeposit: '10', unitType: 'request' }),
5626
6526
  )(req, res)
5627
6527
  if (result.status === 402) return
5628
6528
  res.end('OK')
@@ -5657,22 +6557,20 @@ describe('verifyCredential', () => {
5657
6557
  })
5658
6558
 
5659
6559
  test('verifyCredential charges repeated session voucher content requests when capturedRequest is provided', async () => {
5660
- const escrowContract = await deployEscrow()
5661
6560
  const server = Mppx.create({
5662
6561
  methods: [
5663
- tempo.sessionLegacy({
6562
+ tempo.session({
5664
6563
  store: Store.memory(),
5665
6564
  getClient: () => client,
5666
6565
  account: accounts[0],
5667
6566
  currency: asset,
5668
- escrowContract,
5669
6567
  chainId: client.chain!.id,
5670
6568
  }),
5671
6569
  ],
5672
6570
  realm,
5673
6571
  secretKey,
5674
6572
  })
5675
- const route = server.session({ amount: '1', unitType: 'request' })
6573
+ const route = server.session({ amount: '1', suggestedDeposit: '10', unitType: 'request' })
5676
6574
  const clientMppx = Mppx_client.create({
5677
6575
  polyfill: false,
5678
6576
  methods: [