mppx 0.6.30 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +9 -7
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Constants.d.ts +46 -0
  6. package/dist/Constants.d.ts.map +1 -0
  7. package/dist/Constants.js +46 -0
  8. package/dist/Constants.js.map +1 -0
  9. package/dist/Credential.d.ts.map +1 -1
  10. package/dist/Credential.js +5 -4
  11. package/dist/Credential.js.map +1 -1
  12. package/dist/Method.d.ts +32 -4
  13. package/dist/Method.d.ts.map +1 -1
  14. package/dist/Method.js +5 -2
  15. package/dist/Method.js.map +1 -1
  16. package/dist/Receipt.d.ts.map +1 -1
  17. package/dist/Receipt.js +3 -2
  18. package/dist/Receipt.js.map +1 -1
  19. package/dist/cli/cli.d.ts.map +1 -1
  20. package/dist/cli/cli.js +19 -11
  21. package/dist/cli/cli.js.map +1 -1
  22. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  23. package/dist/cli/plugins/tempo.js +17 -6
  24. package/dist/cli/plugins/tempo.js.map +1 -1
  25. package/dist/cli/utils.d.ts +5 -0
  26. package/dist/cli/utils.d.ts.map +1 -1
  27. package/dist/cli/utils.js +10 -0
  28. package/dist/cli/utils.js.map +1 -1
  29. package/dist/client/Methods.d.ts +5 -2
  30. package/dist/client/Methods.d.ts.map +1 -1
  31. package/dist/client/Methods.js +5 -2
  32. package/dist/client/Methods.js.map +1 -1
  33. package/dist/client/Transport.d.ts.map +1 -1
  34. package/dist/client/Transport.js +4 -5
  35. package/dist/client/Transport.js.map +1 -1
  36. package/dist/client/index.d.ts +2 -1
  37. package/dist/client/index.d.ts.map +1 -1
  38. package/dist/client/index.js +2 -1
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/internal/Fetch.d.ts.map +1 -1
  41. package/dist/client/internal/Fetch.js +14 -6
  42. package/dist/client/internal/Fetch.js.map +1 -1
  43. package/dist/evm/server/Methods.d.ts +1 -1
  44. package/dist/evm/server/Methods.d.ts.map +1 -1
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal/AcceptPayment.d.ts +3 -0
  50. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  51. package/dist/internal/AcceptPayment.js +15 -11
  52. package/dist/internal/AcceptPayment.js.map +1 -1
  53. package/dist/mcp-sdk/client/McpClient.d.ts +12 -5
  54. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  55. package/dist/mcp-sdk/client/McpClient.js +55 -42
  56. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  57. package/dist/server/Mppx.d.ts +11 -3
  58. package/dist/server/Mppx.d.ts.map +1 -1
  59. package/dist/server/Mppx.js +76 -27
  60. package/dist/server/Mppx.js.map +1 -1
  61. package/dist/server/Request.js +24 -10
  62. package/dist/server/Request.js.map +1 -1
  63. package/dist/server/Response.d.ts.map +1 -1
  64. package/dist/server/Response.js +2 -1
  65. package/dist/server/Response.js.map +1 -1
  66. package/dist/server/Transport.d.ts.map +1 -1
  67. package/dist/server/Transport.js +4 -3
  68. package/dist/server/Transport.js.map +1 -1
  69. package/dist/server/index.d.ts +1 -0
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +1 -0
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/stripe/client/Charge.d.ts +1 -1
  74. package/dist/stripe/client/Charge.d.ts.map +1 -1
  75. package/dist/stripe/client/Charge.js +3 -1
  76. package/dist/stripe/client/Charge.js.map +1 -1
  77. package/dist/stripe/server/Charge.d.ts +1 -1
  78. package/dist/stripe/server/Charge.d.ts.map +1 -1
  79. package/dist/stripe/server/Charge.js +9 -2
  80. package/dist/stripe/server/Charge.js.map +1 -1
  81. package/dist/stripe/server/Methods.d.ts +1 -1
  82. package/dist/stripe/server/Methods.d.ts.map +1 -1
  83. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  84. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  85. package/dist/stripe/server/internal/html.gen.js +1 -1
  86. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  87. package/dist/tempo/Methods.d.ts +18 -0
  88. package/dist/tempo/Methods.d.ts.map +1 -1
  89. package/dist/tempo/Methods.js +16 -1
  90. package/dist/tempo/Methods.js.map +1 -1
  91. package/dist/tempo/client/Charge.d.ts +6 -0
  92. package/dist/tempo/client/Charge.d.ts.map +1 -1
  93. package/dist/tempo/client/Charge.js +9 -2
  94. package/dist/tempo/client/Charge.js.map +1 -1
  95. package/dist/tempo/client/Methods.d.ts +36 -7
  96. package/dist/tempo/client/Methods.d.ts.map +1 -1
  97. package/dist/tempo/client/Methods.js +12 -5
  98. package/dist/tempo/client/Methods.js.map +1 -1
  99. package/dist/tempo/client/index.d.ts +7 -4
  100. package/dist/tempo/client/index.d.ts.map +1 -1
  101. package/dist/tempo/client/index.js +5 -3
  102. package/dist/tempo/client/index.js.map +1 -1
  103. package/dist/tempo/index.d.ts +1 -0
  104. package/dist/tempo/index.d.ts.map +1 -1
  105. package/dist/tempo/index.js +1 -0
  106. package/dist/tempo/index.js.map +1 -1
  107. package/dist/tempo/internal/fee-payer.d.ts +21 -1
  108. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  109. package/dist/tempo/internal/fee-payer.js +109 -4
  110. package/dist/tempo/internal/fee-payer.js.map +1 -1
  111. package/dist/tempo/{client → legacy/client}/ChannelOps.d.ts +19 -6
  112. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -0
  113. package/dist/tempo/{client → legacy/client}/ChannelOps.js +9 -3
  114. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -0
  115. package/dist/tempo/{client → legacy/client}/Session.d.ts +23 -4
  116. package/dist/tempo/legacy/client/Session.d.ts.map +1 -0
  117. package/dist/tempo/{client → legacy/client}/Session.js +14 -7
  118. package/dist/tempo/legacy/client/Session.js.map +1 -0
  119. package/dist/tempo/{client → legacy/client}/SessionManager.d.ts +20 -5
  120. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -0
  121. package/dist/tempo/{client → legacy/client}/SessionManager.js +20 -16
  122. package/dist/tempo/legacy/client/SessionManager.js.map +1 -0
  123. package/dist/tempo/legacy/client/index.d.ts +7 -0
  124. package/dist/tempo/legacy/client/index.d.ts.map +1 -0
  125. package/dist/tempo/legacy/client/index.js +5 -0
  126. package/dist/tempo/legacy/client/index.js.map +1 -0
  127. package/dist/tempo/legacy/index.d.ts +7 -0
  128. package/dist/tempo/legacy/index.d.ts.map +1 -0
  129. package/dist/tempo/legacy/index.js +7 -0
  130. package/dist/tempo/legacy/index.js.map +1 -0
  131. package/dist/tempo/{server → legacy/server}/Session.d.ts +28 -11
  132. package/dist/tempo/legacy/server/Session.d.ts.map +1 -0
  133. package/dist/tempo/{server → legacy/server}/Session.js +28 -23
  134. package/dist/tempo/legacy/server/Session.js.map +1 -0
  135. package/dist/tempo/legacy/server/index.d.ts +5 -0
  136. package/dist/tempo/legacy/server/index.d.ts.map +1 -0
  137. package/dist/tempo/legacy/server/index.js +5 -0
  138. package/dist/tempo/legacy/server/index.js.map +1 -0
  139. package/dist/tempo/{session → legacy/session}/Chain.d.ts +30 -23
  140. package/dist/tempo/legacy/session/Chain.d.ts.map +1 -0
  141. package/dist/tempo/{session → legacy/session}/Chain.js +12 -11
  142. package/dist/tempo/legacy/session/Chain.js.map +1 -0
  143. package/dist/tempo/{session → legacy/session}/Channel.d.ts +1 -0
  144. package/dist/tempo/legacy/session/Channel.d.ts.map +1 -0
  145. package/dist/tempo/legacy/session/Channel.js.map +1 -0
  146. package/dist/tempo/legacy/session/ChannelStore.d.ts +22 -0
  147. package/dist/tempo/legacy/session/ChannelStore.d.ts.map +1 -0
  148. package/dist/tempo/legacy/session/ChannelStore.js +6 -0
  149. package/dist/tempo/legacy/session/ChannelStore.js.map +1 -0
  150. package/dist/tempo/legacy/session/Types.d.ts +73 -0
  151. package/dist/tempo/legacy/session/Types.d.ts.map +1 -0
  152. package/dist/tempo/legacy/session/Types.js.map +1 -0
  153. package/dist/tempo/{session → legacy/session}/Voucher.d.ts +4 -4
  154. package/dist/tempo/legacy/session/Voucher.d.ts.map +1 -0
  155. package/dist/tempo/{session → legacy/session}/Voucher.js +1 -1
  156. package/dist/tempo/legacy/session/Voucher.js.map +1 -0
  157. package/dist/tempo/{session → legacy/session}/escrow.abi.d.ts +1 -0
  158. package/dist/tempo/{session → legacy/session}/escrow.abi.d.ts.map +1 -1
  159. package/dist/tempo/{session → legacy/session}/escrow.abi.js +1 -0
  160. package/dist/tempo/legacy/session/escrow.abi.js.map +1 -0
  161. package/dist/tempo/legacy/session/index.d.ts +9 -0
  162. package/dist/tempo/legacy/session/index.d.ts.map +1 -0
  163. package/dist/tempo/legacy/session/index.js +9 -0
  164. package/dist/tempo/legacy/session/index.js.map +1 -0
  165. package/dist/tempo/server/Charge.d.ts +1 -1
  166. package/dist/tempo/server/Charge.d.ts.map +1 -1
  167. package/dist/tempo/server/Charge.js +13 -16
  168. package/dist/tempo/server/Charge.js.map +1 -1
  169. package/dist/tempo/server/Methods.d.ts +63 -6
  170. package/dist/tempo/server/Methods.d.ts.map +1 -1
  171. package/dist/tempo/server/Methods.js +36 -8
  172. package/dist/tempo/server/Methods.js.map +1 -1
  173. package/dist/tempo/server/Subscription.d.ts +1 -1
  174. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  175. package/dist/tempo/server/index.d.ts +6 -5
  176. package/dist/tempo/server/index.d.ts.map +1 -1
  177. package/dist/tempo/server/index.js +5 -5
  178. package/dist/tempo/server/index.js.map +1 -1
  179. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  180. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  181. package/dist/tempo/server/internal/html.gen.js +1 -1
  182. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  183. package/dist/tempo/server/internal/request-body.d.ts +7 -2
  184. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  185. package/dist/tempo/server/internal/request-body.js +20 -3
  186. package/dist/tempo/server/internal/request-body.js.map +1 -1
  187. package/dist/tempo/server/internal/transport.d.ts +8 -4
  188. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  189. package/dist/tempo/server/internal/transport.js +8 -7
  190. package/dist/tempo/server/internal/transport.js.map +1 -1
  191. package/dist/tempo/session/Snapshot.d.ts +32 -0
  192. package/dist/tempo/session/Snapshot.d.ts.map +1 -0
  193. package/dist/tempo/session/Snapshot.js +37 -0
  194. package/dist/tempo/session/Snapshot.js.map +1 -0
  195. package/dist/tempo/session/client/ChannelOps.d.ts +82 -0
  196. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -0
  197. package/dist/tempo/session/client/ChannelOps.js +204 -0
  198. package/dist/tempo/session/client/ChannelOps.js.map +1 -0
  199. package/dist/tempo/session/client/CredentialState.d.ts +262 -0
  200. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -0
  201. package/dist/tempo/session/client/CredentialState.js +417 -0
  202. package/dist/tempo/session/client/CredentialState.js.map +1 -0
  203. package/dist/tempo/session/client/ReceiptCoordinator.d.ts +26 -0
  204. package/dist/tempo/session/client/ReceiptCoordinator.d.ts.map +1 -0
  205. package/dist/tempo/session/client/ReceiptCoordinator.js +61 -0
  206. package/dist/tempo/session/client/ReceiptCoordinator.js.map +1 -0
  207. package/dist/tempo/session/client/Runtime.d.ts +464 -0
  208. package/dist/tempo/session/client/Runtime.d.ts.map +1 -0
  209. package/dist/tempo/session/client/Runtime.js +499 -0
  210. package/dist/tempo/session/client/Runtime.js.map +1 -0
  211. package/dist/tempo/session/client/Session.d.ts +132 -0
  212. package/dist/tempo/session/client/Session.d.ts.map +1 -0
  213. package/dist/tempo/session/client/Session.js +55 -0
  214. package/dist/tempo/session/client/Session.js.map +1 -0
  215. package/dist/tempo/session/client/SessionManager.d.ts +120 -0
  216. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -0
  217. package/dist/tempo/session/client/SessionManager.js +627 -0
  218. package/dist/tempo/session/client/SessionManager.js.map +1 -0
  219. package/dist/tempo/session/client/Transports.d.ts +449 -0
  220. package/dist/tempo/session/client/Transports.d.ts.map +1 -0
  221. package/dist/tempo/session/client/Transports.js +721 -0
  222. package/dist/tempo/session/client/Transports.js.map +1 -0
  223. package/dist/tempo/session/client/index.d.ts +12 -0
  224. package/dist/tempo/session/client/index.d.ts.map +1 -0
  225. package/dist/tempo/session/client/index.js +5 -0
  226. package/dist/tempo/session/client/index.js.map +1 -0
  227. package/dist/tempo/session/index.d.ts +7 -8
  228. package/dist/tempo/session/index.d.ts.map +1 -1
  229. package/dist/tempo/session/index.js +7 -8
  230. package/dist/tempo/session/index.js.map +1 -1
  231. package/dist/tempo/session/precompile/Chain.d.ts +319 -0
  232. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -0
  233. package/dist/tempo/session/precompile/Chain.js +492 -0
  234. package/dist/tempo/session/precompile/Chain.js.map +1 -0
  235. package/dist/tempo/session/precompile/Channel.d.ts +46 -0
  236. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -0
  237. package/dist/tempo/session/precompile/Channel.js +56 -0
  238. package/dist/tempo/session/precompile/Channel.js.map +1 -0
  239. package/dist/tempo/session/precompile/Protocol.d.ts +308 -0
  240. package/dist/tempo/session/precompile/Protocol.d.ts.map +1 -0
  241. package/dist/tempo/session/precompile/Protocol.js +264 -0
  242. package/dist/tempo/session/precompile/Protocol.js.map +1 -0
  243. package/dist/tempo/session/precompile/Voucher.d.ts +40 -0
  244. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -0
  245. package/dist/tempo/session/precompile/Voucher.js +126 -0
  246. package/dist/tempo/session/precompile/Voucher.js.map +1 -0
  247. package/dist/tempo/session/precompile/escrow.abi.d.ts +522 -0
  248. package/dist/tempo/session/precompile/escrow.abi.d.ts.map +1 -0
  249. package/dist/tempo/session/precompile/escrow.abi.js +224 -0
  250. package/dist/tempo/session/precompile/escrow.abi.js.map +1 -0
  251. package/dist/tempo/session/precompile/index.d.ts +24 -0
  252. package/dist/tempo/session/precompile/index.d.ts.map +1 -0
  253. package/dist/tempo/session/precompile/index.js +22 -0
  254. package/dist/tempo/session/precompile/index.js.map +1 -0
  255. package/dist/tempo/session/server/ChannelOps.d.ts +56 -0
  256. package/dist/tempo/session/server/ChannelOps.d.ts.map +1 -0
  257. package/dist/tempo/session/server/ChannelOps.js +91 -0
  258. package/dist/tempo/session/server/ChannelOps.js.map +1 -0
  259. package/dist/tempo/session/server/ChannelStore.d.ts +347 -0
  260. package/dist/tempo/session/server/ChannelStore.d.ts.map +1 -0
  261. package/dist/tempo/session/server/ChannelStore.js +404 -0
  262. package/dist/tempo/session/server/ChannelStore.js.map +1 -0
  263. package/dist/tempo/session/server/CredentialVerification.d.ts +85 -0
  264. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -0
  265. package/dist/tempo/session/server/CredentialVerification.js +494 -0
  266. package/dist/tempo/session/server/CredentialVerification.js.map +1 -0
  267. package/dist/tempo/session/server/MeteredStream.d.ts +40 -0
  268. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -0
  269. package/dist/tempo/session/server/MeteredStream.js +42 -0
  270. package/dist/tempo/session/server/MeteredStream.js.map +1 -0
  271. package/dist/tempo/session/server/RequestState.d.ts +208 -0
  272. package/dist/tempo/session/server/RequestState.d.ts.map +1 -0
  273. package/dist/tempo/session/server/RequestState.js +252 -0
  274. package/dist/tempo/session/server/RequestState.js.map +1 -0
  275. package/dist/tempo/session/server/Session.d.ts +169 -0
  276. package/dist/tempo/session/server/Session.d.ts.map +1 -0
  277. package/dist/tempo/session/server/Session.js +351 -0
  278. package/dist/tempo/session/server/Session.js.map +1 -0
  279. package/dist/tempo/session/server/Settlement.d.ts +185 -0
  280. package/dist/tempo/session/server/Settlement.d.ts.map +1 -0
  281. package/dist/tempo/session/server/Settlement.js +250 -0
  282. package/dist/tempo/session/server/Settlement.js.map +1 -0
  283. package/dist/tempo/session/{Sse.d.ts → server/Sse.d.ts} +9 -56
  284. package/dist/tempo/session/server/Sse.d.ts.map +1 -0
  285. package/dist/tempo/session/server/Sse.js +184 -0
  286. package/dist/tempo/session/server/Sse.js.map +1 -0
  287. package/dist/tempo/session/server/Transports.d.ts +89 -0
  288. package/dist/tempo/session/server/Transports.d.ts.map +1 -0
  289. package/dist/tempo/session/server/Transports.js +149 -0
  290. package/dist/tempo/session/server/Transports.js.map +1 -0
  291. package/dist/tempo/session/server/Ws.d.ts +48 -0
  292. package/dist/tempo/session/server/Ws.d.ts.map +1 -0
  293. package/dist/tempo/session/server/Ws.js +244 -0
  294. package/dist/tempo/session/server/Ws.js.map +1 -0
  295. package/dist/tempo/session/server/index.d.ts +4 -0
  296. package/dist/tempo/session/server/index.d.ts.map +1 -0
  297. package/dist/tempo/session/server/index.js +2 -0
  298. package/dist/tempo/session/server/index.js.map +1 -0
  299. package/package.json +8 -3
  300. package/src/Challenge.ts +9 -7
  301. package/src/Constants.ts +58 -0
  302. package/src/Credential.ts +5 -4
  303. package/src/Method.ts +46 -5
  304. package/src/Receipt.ts +3 -2
  305. package/src/cli/cli.test.ts +23 -28
  306. package/src/cli/cli.ts +23 -10
  307. package/src/cli/mcp.test.ts +21 -7
  308. package/src/cli/plugins/tempo.ts +21 -8
  309. package/src/cli/utils.test.ts +25 -1
  310. package/src/cli/utils.ts +10 -0
  311. package/src/client/Methods.ts +5 -2
  312. package/src/client/Mppx.test-d.ts +10 -0
  313. package/src/client/Mppx.test.ts +75 -0
  314. package/src/client/Transport.ts +4 -5
  315. package/src/client/index.ts +11 -1
  316. package/src/client/internal/Fetch.test.ts +29 -4
  317. package/src/client/internal/Fetch.ts +17 -5
  318. package/src/env.d.ts +1 -1
  319. package/src/index.ts +1 -0
  320. package/src/internal/AcceptPayment.test.ts +61 -0
  321. package/src/internal/AcceptPayment.ts +21 -14
  322. package/src/mcp-sdk/client/McpClient.integration.test.ts +8 -7
  323. package/src/mcp-sdk/client/McpClient.test-d.ts +7 -0
  324. package/src/mcp-sdk/client/McpClient.ts +99 -67
  325. package/src/mcp-sdk/client/McpClient.unit.test.ts +131 -0
  326. package/src/middlewares/elysia.test.ts +8 -4
  327. package/src/middlewares/express.test.ts +8 -4
  328. package/src/middlewares/hono.test.ts +4 -4
  329. package/src/middlewares/nextjs.test.ts +8 -4
  330. package/src/proxy/Proxy.test.ts +8 -8
  331. package/src/server/Mppx.test-d.ts +54 -0
  332. package/src/server/Mppx.test.ts +274 -7
  333. package/src/server/Mppx.ts +487 -406
  334. package/src/server/Request.test.ts +81 -0
  335. package/src/server/Request.ts +23 -9
  336. package/src/server/Response.ts +2 -1
  337. package/src/server/Transport.ts +4 -3
  338. package/src/server/index.ts +1 -0
  339. package/src/stripe/client/Charge.test.ts +20 -5
  340. package/src/stripe/client/Charge.ts +6 -2
  341. package/src/stripe/server/Charge.test.ts +114 -1
  342. package/src/stripe/server/Charge.ts +13 -2
  343. package/src/stripe/server/internal/html/package.json +1 -1
  344. package/src/stripe/server/internal/html.gen.ts +1 -1
  345. package/src/tempo/AccessKeyAuthorization.test.ts +4 -94
  346. package/src/tempo/Methods.test.ts +45 -17
  347. package/src/tempo/Methods.ts +22 -0
  348. package/src/tempo/PublicExports.test-d.ts +105 -0
  349. package/src/tempo/client/Charge.test.ts +85 -0
  350. package/src/tempo/client/Charge.ts +19 -2
  351. package/src/tempo/client/Methods.ts +18 -6
  352. package/src/tempo/client/index.ts +15 -4
  353. package/src/tempo/index.ts +1 -0
  354. package/src/tempo/internal/fee-payer.test.ts +241 -17
  355. package/src/tempo/internal/fee-payer.ts +150 -4
  356. package/src/tempo/internal/fee-token.test.ts +14 -9
  357. package/src/tempo/legacy/AccessKeyAuthorization.test.ts +162 -0
  358. package/src/tempo/legacy/README.md +9 -0
  359. package/src/tempo/{client → legacy/client}/ChannelOps.test.ts +6 -7
  360. package/src/tempo/{client → legacy/client}/ChannelOps.ts +22 -9
  361. package/src/tempo/{client → legacy/client}/Session.test.ts +51 -9
  362. package/src/tempo/{client → legacy/client}/Session.ts +25 -11
  363. package/src/tempo/{client → legacy/client}/SessionManager.test.ts +81 -9
  364. package/src/tempo/{client → legacy/client}/SessionManager.ts +41 -20
  365. package/src/tempo/legacy/client/index.ts +6 -0
  366. package/src/tempo/legacy/index.ts +6 -0
  367. package/src/tempo/{server → legacy/server}/Session.test.ts +162 -63
  368. package/src/tempo/{server → legacy/server}/Session.ts +49 -37
  369. package/src/tempo/legacy/server/index.ts +4 -0
  370. package/src/tempo/{session → legacy/session}/Chain.test.ts +3 -4
  371. package/src/tempo/{session → legacy/session}/Chain.ts +94 -63
  372. package/src/tempo/{session → legacy/session}/Channel.ts +1 -0
  373. package/src/tempo/legacy/session/ChannelStore.test.ts +58 -0
  374. package/src/tempo/legacy/session/ChannelStore.ts +39 -0
  375. package/src/tempo/legacy/session/Types.ts +91 -0
  376. package/src/tempo/{session → legacy/session}/Voucher.ts +12 -8
  377. package/src/tempo/{session → legacy/session}/escrow.abi.ts +1 -0
  378. package/src/tempo/legacy/session/index.ts +8 -0
  379. package/src/tempo/server/AtomicStore.test-d.ts +16 -11
  380. package/src/tempo/server/Charge.test.ts +92 -14
  381. package/src/tempo/server/Charge.ts +18 -16
  382. package/src/tempo/server/Methods.ts +54 -8
  383. package/src/tempo/server/Sse.test.ts +2 -2
  384. package/src/tempo/server/index.ts +6 -5
  385. package/src/tempo/server/internal/html/package.json +1 -1
  386. package/src/tempo/server/internal/html.gen.ts +1 -1
  387. package/src/tempo/server/internal/request-body.test.ts +37 -4
  388. package/src/tempo/server/internal/request-body.ts +25 -6
  389. package/src/tempo/server/internal/transport.test.ts +4 -4
  390. package/src/tempo/server/internal/transport.ts +19 -10
  391. package/src/tempo/session/Snapshot.test.ts +41 -0
  392. package/src/tempo/session/Snapshot.ts +74 -0
  393. package/src/tempo/session/client/ChannelOps.test.ts +163 -0
  394. package/src/tempo/session/client/ChannelOps.ts +344 -0
  395. package/src/tempo/session/client/CredentialState.test.ts +645 -0
  396. package/src/tempo/session/client/CredentialState.ts +814 -0
  397. package/src/tempo/session/client/ReceiptCoordinator.ts +95 -0
  398. package/src/tempo/session/client/Runtime.test.ts +1092 -0
  399. package/src/tempo/session/client/Runtime.ts +986 -0
  400. package/src/tempo/session/client/Session.test.ts +734 -0
  401. package/src/tempo/session/client/Session.ts +97 -0
  402. package/src/tempo/session/client/SessionManager.test.ts +1308 -0
  403. package/src/tempo/session/client/SessionManager.ts +845 -0
  404. package/src/tempo/session/client/Transports.test.ts +837 -0
  405. package/src/tempo/session/client/Transports.ts +1292 -0
  406. package/src/tempo/session/client/index.ts +37 -0
  407. package/src/tempo/session/index.ts +7 -8
  408. package/src/tempo/session/precompile/Chain.integration.test.ts +321 -0
  409. package/src/tempo/session/precompile/Chain.test.ts +1258 -0
  410. package/src/tempo/session/precompile/Chain.ts +979 -0
  411. package/src/tempo/session/precompile/Channel.test.ts +138 -0
  412. package/src/tempo/session/precompile/Channel.ts +103 -0
  413. package/src/tempo/session/precompile/Protocol.test.ts +358 -0
  414. package/src/tempo/session/precompile/Protocol.ts +520 -0
  415. package/src/tempo/session/precompile/Voucher.test.ts +316 -0
  416. package/src/tempo/session/precompile/Voucher.ts +160 -0
  417. package/src/tempo/session/precompile/escrow.abi.ts +226 -0
  418. package/src/tempo/session/precompile/index.ts +33 -0
  419. package/src/tempo/session/server/ChannelOps.test.ts +129 -0
  420. package/src/tempo/session/server/ChannelOps.ts +157 -0
  421. package/src/tempo/session/{ChannelStore.test.ts → server/ChannelStore.test.ts} +536 -29
  422. package/src/tempo/session/server/ChannelStore.ts +835 -0
  423. package/src/tempo/session/server/CredentialVerification.test.ts +146 -0
  424. package/src/tempo/session/server/CredentialVerification.ts +710 -0
  425. package/src/tempo/session/server/MeteredStream.ts +88 -0
  426. package/src/tempo/session/server/RequestState.test.ts +531 -0
  427. package/src/tempo/session/server/RequestState.ts +499 -0
  428. package/src/tempo/session/server/Session.integration.test.ts +444 -0
  429. package/src/tempo/session/server/Session.test.ts +3253 -0
  430. package/src/tempo/session/server/Session.ts +543 -0
  431. package/src/tempo/session/server/Settlement.test.ts +242 -0
  432. package/src/tempo/session/server/Settlement.ts +470 -0
  433. package/src/tempo/session/{Sse.test.ts → server/Sse.test.ts} +37 -3
  434. package/src/tempo/session/server/Sse.ts +256 -0
  435. package/src/tempo/session/server/Transports.test.ts +346 -0
  436. package/src/tempo/session/server/Transports.ts +255 -0
  437. package/src/tempo/session/{Ws.test.ts → server/Ws.test.ts} +4 -4
  438. package/src/tempo/session/server/Ws.ts +384 -0
  439. package/src/tempo/session/server/index.ts +8 -0
  440. package/dist/tempo/client/ChannelOps.d.ts.map +0 -1
  441. package/dist/tempo/client/ChannelOps.js.map +0 -1
  442. package/dist/tempo/client/Session.d.ts.map +0 -1
  443. package/dist/tempo/client/Session.js.map +0 -1
  444. package/dist/tempo/client/SessionManager.d.ts.map +0 -1
  445. package/dist/tempo/client/SessionManager.js.map +0 -1
  446. package/dist/tempo/server/Session.d.ts.map +0 -1
  447. package/dist/tempo/server/Session.js.map +0 -1
  448. package/dist/tempo/session/Chain.d.ts.map +0 -1
  449. package/dist/tempo/session/Chain.js.map +0 -1
  450. package/dist/tempo/session/Channel.d.ts.map +0 -1
  451. package/dist/tempo/session/Channel.js.map +0 -1
  452. package/dist/tempo/session/ChannelStore.d.ts +0 -117
  453. package/dist/tempo/session/ChannelStore.d.ts.map +0 -1
  454. package/dist/tempo/session/ChannelStore.js +0 -172
  455. package/dist/tempo/session/ChannelStore.js.map +0 -1
  456. package/dist/tempo/session/Receipt.d.ts +0 -22
  457. package/dist/tempo/session/Receipt.d.ts.map +0 -1
  458. package/dist/tempo/session/Receipt.js +0 -34
  459. package/dist/tempo/session/Receipt.js.map +0 -1
  460. package/dist/tempo/session/Sse.d.ts.map +0 -1
  461. package/dist/tempo/session/Sse.js +0 -363
  462. package/dist/tempo/session/Sse.js.map +0 -1
  463. package/dist/tempo/session/Types.d.ts +0 -78
  464. package/dist/tempo/session/Types.d.ts.map +0 -1
  465. package/dist/tempo/session/Types.js.map +0 -1
  466. package/dist/tempo/session/Voucher.d.ts.map +0 -1
  467. package/dist/tempo/session/Voucher.js.map +0 -1
  468. package/dist/tempo/session/Ws.d.ts +0 -87
  469. package/dist/tempo/session/Ws.d.ts.map +0 -1
  470. package/dist/tempo/session/Ws.js +0 -443
  471. package/dist/tempo/session/Ws.js.map +0 -1
  472. package/dist/tempo/session/escrow.abi.js.map +0 -1
  473. package/src/tempo/session/ChannelStore.ts +0 -308
  474. package/src/tempo/session/Receipt.test.ts +0 -89
  475. package/src/tempo/session/Receipt.ts +0 -46
  476. package/src/tempo/session/Sse.ts +0 -462
  477. package/src/tempo/session/Types.ts +0 -86
  478. package/src/tempo/session/Ws.ts +0 -576
  479. /package/dist/tempo/{session → legacy/session}/Channel.js +0 -0
  480. /package/dist/tempo/{session → legacy/session}/Types.js +0 -0
  481. /package/src/tempo/{session → legacy/session}/Channel.test.ts +0 -0
  482. /package/src/tempo/{session → legacy/session}/Voucher.test.ts +0 -0
  483. /package/src/tempo/session/{Sse.fuzz.test.ts → server/Sse.fuzz.test.ts} +0 -0
@@ -0,0 +1,1308 @@
1
+ import { createClient, custom, encodeFunctionResult, type Address, type Hex } from 'viem'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+ import { describe, expect, test, vi } from 'vp/test'
4
+
5
+ import * as Challenge from '../../../Challenge.js'
6
+ import * as Constants from '../../../Constants.js'
7
+ import * as Credential from '../../../Credential.js'
8
+ import * as Channel from '../precompile/Channel.js'
9
+ import { escrowAbi } from '../precompile/escrow.abi.js'
10
+ import { tip20ChannelEscrow } from '../precompile/Protocol.js'
11
+ import { createSessionReceipt, serializeSessionReceipt } from '../precompile/Protocol.js'
12
+ import type { NeedVoucherEvent, SessionReceipt } from '../precompile/Protocol.js'
13
+ import { formatNeedVoucherEvent, parseEvent } from '../precompile/Protocol.js'
14
+ import type { SessionCredentialPayload } from '../precompile/Protocol.js'
15
+ import { computeFallbackCloseAmount, sessionManager } from './SessionManager.js'
16
+ import type { StoredSessionChannel } from './SessionManager.js'
17
+
18
+ const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
19
+ const challengeId = 'test-challenge-1'
20
+ const realm = 'test.example.com'
21
+ const account = privateKeyToAccount(
22
+ '0xac0974bec39a17e36ba6a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
23
+ )
24
+
25
+ const client = createClient({
26
+ account,
27
+ chain: { id: 4217 } as never,
28
+ transport: custom({
29
+ async request(args) {
30
+ if (args.method === 'eth_chainId') return '0x1079'
31
+ if (args.method === 'eth_getTransactionCount') return '0x0'
32
+ if (args.method === 'eth_estimateGas') return '0x5208'
33
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
34
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
35
+ if (args.method === 'eth_call')
36
+ return encodeFunctionResult({
37
+ abi: escrowAbi,
38
+ functionName: 'getChannelState',
39
+ result: { settled: 0n, deposit: 10_000_000n, closeRequestedAt: 0 },
40
+ })
41
+ throw new Error(`unexpected rpc request: ${args.method}`)
42
+ },
43
+ }),
44
+ })
45
+
46
+ const storedDescriptor = {
47
+ authorizedSigner: account.address,
48
+ expiringNonceHash: `0x${'11'.repeat(32)}` as Hex,
49
+ operator: '0x0000000000000000000000000000000000000000' as Address,
50
+ payee: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00' as Address,
51
+ payer: account.address,
52
+ salt: `0x${'22'.repeat(32)}` as Hex,
53
+ token: '0x20c0000000000000000000000000000000000001' as Address,
54
+ }
55
+
56
+ const storedChannelId = Channel.computeId({
57
+ ...storedDescriptor,
58
+ chainId: 4217,
59
+ escrow: tip20ChannelEscrow,
60
+ })
61
+
62
+ function storedChannel(overrides: Partial<StoredSessionChannel> = {}): StoredSessionChannel {
63
+ return {
64
+ channelId: storedChannelId,
65
+ cumulativeAmount: '1000000',
66
+ deposit: '10000000',
67
+ descriptor: storedDescriptor,
68
+ escrow: tip20ChannelEscrow,
69
+ chainId: 4217,
70
+ opened: true,
71
+ updatedAt: 0,
72
+ ...overrides,
73
+ }
74
+ }
75
+
76
+ function makeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
77
+ return Challenge.from({
78
+ id: challengeId,
79
+ realm,
80
+ method: 'tempo',
81
+ intent: 'session',
82
+ request: {
83
+ amount: '1000000',
84
+ currency: '0x20c0000000000000000000000000000000000001',
85
+ recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
86
+ decimals: 6,
87
+ methodDetails: {
88
+ escrowContract: tip20ChannelEscrow,
89
+ chainId: 4217,
90
+ sessionProtocol: Constants.SessionProtocols.v2,
91
+ },
92
+ ...overrides,
93
+ },
94
+ })
95
+ }
96
+
97
+ function makeChargeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
98
+ return Challenge.from({
99
+ id: 'charge-bootstrap',
100
+ realm,
101
+ method: 'tempo',
102
+ intent: 'charge',
103
+ request: {
104
+ amount: '0',
105
+ currency: '0x20c0000000000000000000000000000000000001',
106
+ recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
107
+ methodDetails: { chainId: 4217 },
108
+ ...overrides,
109
+ },
110
+ })
111
+ }
112
+
113
+ function make402Response(challenge?: Challenge.Challenge): Response {
114
+ const c = challenge ?? makeChallenge()
115
+ return new Response(null, {
116
+ status: 402,
117
+ headers: { 'WWW-Authenticate': Challenge.serialize(c) },
118
+ })
119
+ }
120
+
121
+ function makeOkResponse(body?: string): Response {
122
+ return new Response(body ?? 'ok', { status: 200 })
123
+ }
124
+
125
+ function makeSseResponse(events: string[]): Response {
126
+ const body = events.join('')
127
+ return new Response(body, {
128
+ status: 200,
129
+ headers: { 'Content-Type': 'text/event-stream' },
130
+ })
131
+ }
132
+
133
+ describe('Session', () => {
134
+ describe('computeFallbackCloseAmount', () => {
135
+ test('uses matching close-ready receipt spend first', () => {
136
+ const receipt = createSessionReceipt({
137
+ acceptedCumulative: 100n,
138
+ challengeId,
139
+ channelId,
140
+ spent: 80n,
141
+ })
142
+
143
+ expect(
144
+ computeFallbackCloseAmount({
145
+ challengeId,
146
+ channelId,
147
+ closeReadyReceipt: receipt,
148
+ cumulativeAmount: 100n,
149
+ deliveredChunks: 10n,
150
+ socketChallengeId: challengeId,
151
+ socketChannelId: channelId,
152
+ spent: 50n,
153
+ tickCost: 10n,
154
+ }),
155
+ ).toBe(80n)
156
+ })
157
+
158
+ test('uses matching socket delivery estimate clamped to cumulative authorization', () => {
159
+ expect(
160
+ computeFallbackCloseAmount({
161
+ challengeId,
162
+ channelId,
163
+ cumulativeAmount: 90n,
164
+ deliveredChunks: 10n,
165
+ socketChallengeId: challengeId,
166
+ socketChannelId: channelId,
167
+ spent: 40n,
168
+ tickCost: 10n,
169
+ }),
170
+ ).toBe(90n)
171
+ })
172
+
173
+ test('uses receipt-tracked spend when no socket estimate applies', () => {
174
+ expect(
175
+ computeFallbackCloseAmount({
176
+ challengeId,
177
+ channelId,
178
+ cumulativeAmount: 100n,
179
+ deliveredChunks: 10n,
180
+ socketChallengeId: 'other-challenge',
181
+ socketChannelId: channelId,
182
+ spent: 40n,
183
+ tickCost: 10n,
184
+ }),
185
+ ).toBe(40n)
186
+ })
187
+ })
188
+
189
+ describe('parseEvent round-trip via SSE', () => {
190
+ test('parses message events from SSE stream', () => {
191
+ const raw = 'event: message\ndata: hello world\n\n'
192
+ const event = parseEvent(raw)
193
+ expect(event).toEqual({ type: 'message', data: 'hello world' })
194
+ })
195
+
196
+ test('parses payment-need-voucher events', () => {
197
+ const params: NeedVoucherEvent = {
198
+ channelId,
199
+ requiredCumulative: '6000000',
200
+ acceptedCumulative: '5000000',
201
+ deposit: '10000000',
202
+ }
203
+ const raw = formatNeedVoucherEvent(params)
204
+ const event = parseEvent(raw)
205
+ expect(event).toEqual({ type: 'payment-need-voucher', data: params })
206
+ })
207
+ })
208
+
209
+ describe('session creation', () => {
210
+ test('creates session with initial state', () => {
211
+ const s = sessionManager({
212
+ account: '0x0000000000000000000000000000000000000001',
213
+ maxDeposit: '10',
214
+ })
215
+
216
+ expect(s.channelId).toBeUndefined()
217
+ expect(s.cumulative).toBe(0n)
218
+ expect(s.opened).toBe(false)
219
+ })
220
+ })
221
+
222
+ describe('.fetch()', () => {
223
+ test('passes through non-402 responses', async () => {
224
+ const mockFetch = vi.fn().mockResolvedValue(makeOkResponse('hello'))
225
+
226
+ const s = sessionManager({
227
+ account: '0x0000000000000000000000000000000000000001',
228
+ fetch: mockFetch as typeof globalThis.fetch,
229
+ })
230
+
231
+ const res = await s.fetch('https://api.example.com/data')
232
+ expect(res.status).toBe(200)
233
+ expect(await res.text()).toBe('hello')
234
+ expect(mockFetch).toHaveBeenCalledOnce()
235
+ })
236
+
237
+ test('binds the default global fetch for browser runtimes', async () => {
238
+ const originalFetch = globalThis.fetch
239
+ const mockFetch = vi.fn(function (this: unknown) {
240
+ expect(this).toBe(globalThis)
241
+ return Promise.resolve(makeOkResponse('hello'))
242
+ })
243
+ globalThis.fetch = mockFetch as typeof globalThis.fetch
244
+ try {
245
+ const s = sessionManager({
246
+ account: '0x0000000000000000000000000000000000000001',
247
+ })
248
+
249
+ const res = await s.fetch('https://api.example.com/data')
250
+
251
+ expect(res.status).toBe(200)
252
+ expect(mockFetch).toHaveBeenCalledOnce()
253
+ } finally {
254
+ globalThis.fetch = originalFetch
255
+ }
256
+ })
257
+
258
+ test('adds a stored channel hint to the first request', async () => {
259
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
260
+ expect(new Headers(init?.headers).get('Payment-Session')).toBe(storedChannelId)
261
+ return Promise.resolve(makeOkResponse())
262
+ })
263
+ const s = sessionManager({
264
+ account,
265
+ client,
266
+ fetch: mockFetch as typeof globalThis.fetch,
267
+ sessionStore: {
268
+ get: () => storedChannel(),
269
+ set: vi.fn(),
270
+ },
271
+ })
272
+
273
+ await s.fetch('https://api.example.com/data')
274
+
275
+ expect(mockFetch).toHaveBeenCalledOnce()
276
+ })
277
+
278
+ test('stores same-route HEAD snapshot as a first-request channel hint', async () => {
279
+ const set = vi.fn()
280
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
281
+ const headers = new Headers(init?.headers)
282
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
283
+ expect(headers.get(Constants.Headers.acceptPayment)).toBe('tempo/charge')
284
+ return Promise.resolve(make402Response(makeChargeChallenge()))
285
+ }
286
+ if (init?.method === 'HEAD') {
287
+ const credential = Credential.deserialize(headers.get(Constants.Headers.authorization)!)
288
+ expect(credential.payload).toMatchObject({ type: 'proof' })
289
+ return Promise.resolve(
290
+ new Response(null, {
291
+ status: 204,
292
+ headers: {
293
+ [Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
294
+ acceptedCumulative: '1000000',
295
+ chainId: 4217,
296
+ channelId: storedChannelId,
297
+ deposit: '10000000',
298
+ descriptor: storedDescriptor,
299
+ escrow: tip20ChannelEscrow,
300
+ requiredCumulative: '1000000',
301
+ settled: '0',
302
+ spent: '0',
303
+ units: 0,
304
+ }),
305
+ },
306
+ }),
307
+ )
308
+ }
309
+ expect(headers.get(Constants.Headers.paymentSession)).toBe(storedChannelId)
310
+ return Promise.resolve(makeOkResponse())
311
+ })
312
+ const s = sessionManager({
313
+ account,
314
+ bootstrap: true,
315
+ client,
316
+ fetch: mockFetch as typeof globalThis.fetch,
317
+ sessionStore: {
318
+ get: () => null,
319
+ set,
320
+ },
321
+ })
322
+
323
+ const response = await s.fetch('https://api.example.com/data')
324
+
325
+ expect(response.status).toBe(200)
326
+ expect(response.channelId).toBeNull()
327
+ expect(s.channelId).toBeUndefined()
328
+ expect(s.cumulative).toBe(0n)
329
+ expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
330
+ expect(mockFetch).toHaveBeenCalledTimes(3)
331
+ })
332
+
333
+ test('does not answer non-zero bootstrap charge challenges', async () => {
334
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
335
+ const headers = new Headers(init?.headers)
336
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
337
+ return Promise.resolve(make402Response(makeChargeChallenge({ amount: '1' })))
338
+ }
339
+ if (init?.method === 'HEAD') throw new Error('unexpected bootstrap authorization')
340
+ expect(headers.get(Constants.Headers.paymentSession)).toBeNull()
341
+ return Promise.resolve(makeOkResponse())
342
+ })
343
+ const s = sessionManager({
344
+ account,
345
+ bootstrap: true,
346
+ client,
347
+ fetch: mockFetch as typeof globalThis.fetch,
348
+ })
349
+
350
+ const response = await s.fetch('https://api.example.com/data')
351
+
352
+ expect(response.status).toBe(200)
353
+ expect(mockFetch).toHaveBeenCalledTimes(2)
354
+ })
355
+
356
+ test('falls back to normal fetch when bootstrap is unsupported', async () => {
357
+ const mockFetch = vi
358
+ .fn()
359
+ .mockResolvedValueOnce(new Response(null, { status: 404 }))
360
+ .mockResolvedValueOnce(makeOkResponse())
361
+ const s = sessionManager({
362
+ account,
363
+ bootstrap: true,
364
+ client,
365
+ fetch: mockFetch as typeof globalThis.fetch,
366
+ })
367
+
368
+ const response = await s.fetch('https://api.example.com/data')
369
+
370
+ expect(response.status).toBe(200)
371
+ expect(s.channelId).toBeUndefined()
372
+ expect(mockFetch).toHaveBeenCalledTimes(2)
373
+ expect(mockFetch.mock.calls[0]?.[1]).toMatchObject({ method: 'HEAD' })
374
+ expect(new Headers(mockFetch.mock.calls[1]?.[1]?.headers).get('Payment-Session')).toBeNull()
375
+ })
376
+
377
+ test('clears stale stored channel hints and retries with a fresh channel', async () => {
378
+ const remove = vi.fn()
379
+ const staleClient = createClient({
380
+ account,
381
+ chain: { id: 4217 } as never,
382
+ transport: custom({
383
+ async request(args) {
384
+ if (args.method === 'eth_chainId') return '0x1079'
385
+ if (args.method === 'eth_getTransactionCount') return '0x0'
386
+ if (args.method === 'eth_estimateGas') return '0x5208'
387
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
388
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
389
+ if (args.method === 'eth_call')
390
+ return encodeFunctionResult({
391
+ abi: escrowAbi,
392
+ functionName: 'getChannelState',
393
+ result: { settled: 0n, deposit: 0n, closeRequestedAt: 0 },
394
+ })
395
+ throw new Error(`unexpected rpc request: ${args.method}`)
396
+ },
397
+ }),
398
+ })
399
+ const postedPayloads: SessionCredentialPayload[] = []
400
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
401
+ const headers = new Headers(init?.headers)
402
+ const authorization = headers.get(Constants.Headers.authorization)
403
+ const payload = authorization
404
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
405
+ : undefined
406
+ if (payload) postedPayloads.push(payload)
407
+
408
+ if (init?.method === 'HEAD') return Promise.resolve(new Response(null, { status: 204 }))
409
+ if (!payload) return Promise.resolve(make402Response())
410
+ return Promise.resolve(makeOkResponse())
411
+ })
412
+ const s = sessionManager({
413
+ account,
414
+ bootstrap: true,
415
+ client: staleClient,
416
+ fetch: mockFetch as typeof globalThis.fetch,
417
+ maxDeposit: '10',
418
+ sessionStore: {
419
+ get: () => storedChannel(),
420
+ set: vi.fn(),
421
+ delete: remove,
422
+ },
423
+ })
424
+
425
+ const response = await s.fetch('https://api.example.com/data')
426
+
427
+ expect(response.status).toBe(200)
428
+ expect(remove).toHaveBeenCalledOnce()
429
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open'])
430
+ expect(s.opened).toBe(true)
431
+ expect(s.channelId).not.toBe(storedChannelId)
432
+ })
433
+
434
+ test('does not bootstrap when disabled', async () => {
435
+ const mockFetch = vi.fn().mockResolvedValue(makeOkResponse())
436
+ const s = sessionManager({
437
+ account,
438
+ client,
439
+ fetch: mockFetch as typeof globalThis.fetch,
440
+ })
441
+
442
+ await s.fetch('https://api.example.com/data')
443
+
444
+ expect(mockFetch).toHaveBeenCalledOnce()
445
+ expect(new Headers(mockFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
446
+ })
447
+
448
+ test('uses stored channel details when the server does not return a snapshot', async () => {
449
+ const postedPayloads: SessionCredentialPayload[] = []
450
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
451
+ const authorization = new Headers(init?.headers).get('Authorization')
452
+ const payload = authorization
453
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
454
+ : undefined
455
+ if (payload) postedPayloads.push(payload)
456
+
457
+ if (!payload) return Promise.resolve(make402Response())
458
+ return Promise.resolve(makeOkResponse())
459
+ })
460
+ const s = sessionManager({
461
+ account,
462
+ client,
463
+ fetch: mockFetch as typeof globalThis.fetch,
464
+ sessionStore: {
465
+ get: () => storedChannel(),
466
+ set: vi.fn(),
467
+ },
468
+ })
469
+
470
+ await s.fetch('https://api.example.com/data')
471
+
472
+ expect(postedPayloads[0]).toMatchObject({
473
+ action: 'voucher',
474
+ channelId: storedChannelId,
475
+ })
476
+ })
477
+
478
+ test('persists opened channels and deletes closed channels when supported', async () => {
479
+ const set = vi.fn()
480
+ const remove = vi.fn()
481
+ let callCount = 0
482
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
483
+ const authorization = new Headers(init?.headers).get('Authorization')
484
+ const payload = authorization
485
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
486
+ : undefined
487
+ callCount++
488
+
489
+ if (callCount === 1) return Promise.resolve(make402Response())
490
+ if (payload?.action === 'open') {
491
+ return Promise.resolve(
492
+ new Response('ok', {
493
+ headers: {
494
+ 'Payment-Receipt': serializeSessionReceipt(
495
+ createSessionReceipt({
496
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
497
+ challengeId,
498
+ channelId: payload.channelId,
499
+ spent: 0n,
500
+ }),
501
+ ),
502
+ },
503
+ }),
504
+ )
505
+ }
506
+ if (payload?.action === 'close') {
507
+ return Promise.resolve(
508
+ new Response('ok', {
509
+ headers: {
510
+ 'Payment-Receipt': serializeSessionReceipt(
511
+ createSessionReceipt({
512
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
513
+ challengeId,
514
+ channelId: payload.channelId,
515
+ spent: BigInt(payload.cumulativeAmount),
516
+ txHash: `0x${'aa'.repeat(32)}`,
517
+ }),
518
+ ),
519
+ },
520
+ }),
521
+ )
522
+ }
523
+ return Promise.resolve(makeOkResponse())
524
+ })
525
+ const s = sessionManager({
526
+ account,
527
+ client,
528
+ fetch: mockFetch as typeof globalThis.fetch,
529
+ maxDeposit: '10',
530
+ sessionStore: {
531
+ get: () => null,
532
+ set,
533
+ delete: remove,
534
+ },
535
+ })
536
+
537
+ await s.fetch('https://api.example.com/data')
538
+ expect(set).toHaveBeenCalledWith(
539
+ expect.objectContaining({
540
+ channelId: s.channelId,
541
+ opened: true,
542
+ }),
543
+ )
544
+
545
+ await s.close()
546
+ expect(remove).toHaveBeenCalledOnce()
547
+ })
548
+
549
+ test('rolls back state when opening a channel throws', async () => {
550
+ const failingClient = createClient({
551
+ account,
552
+ chain: { id: 4217 } as never,
553
+ transport: custom({
554
+ async request(args) {
555
+ if (args.method === 'eth_chainId') return '0x1079'
556
+ if (args.method === 'eth_getTransactionCount') return '0x0'
557
+ if (args.method === 'eth_estimateGas') throw new Error('insufficient balance')
558
+ if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
559
+ if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
560
+ throw new Error(`unexpected rpc request: ${args.method}`)
561
+ },
562
+ }),
563
+ })
564
+ const mockFetch = vi.fn().mockResolvedValue(make402Response())
565
+ const s = sessionManager({
566
+ account,
567
+ client: failingClient,
568
+ fetch: mockFetch as typeof globalThis.fetch,
569
+ maxDeposit: '10',
570
+ })
571
+
572
+ await expect(s.fetch('https://api.example.com/data')).rejects.toThrow(/insufficient balance/)
573
+
574
+ expect(s.state).toEqual({ status: 'idle' })
575
+ expect(s.opened).toBe(false)
576
+ expect(s.cumulative).toBe(0n)
577
+ })
578
+
579
+ test('automatically top-ups and retries when an HTTP session exceeds deposit', async () => {
580
+ const postedPayloads: SessionCredentialPayload[] = []
581
+ let callCount = 0
582
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
583
+ callCount++
584
+ const authorization = new Headers(init?.headers).get('Authorization')
585
+ const payload = authorization
586
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
587
+ : undefined
588
+ if (payload) postedPayloads.push(payload)
589
+
590
+ if (callCount === 1)
591
+ return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '1000000' })))
592
+
593
+ if (payload?.action === 'open') {
594
+ return Promise.resolve(
595
+ make402Response(
596
+ makeChallenge({
597
+ methodDetails: {
598
+ escrowContract: tip20ChannelEscrow,
599
+ chainId: 4217,
600
+ sessionSnapshot: {
601
+ acceptedCumulative: '2000000',
602
+ chainId: 4217,
603
+ channelId: payload.channelId,
604
+ deposit: '1000000',
605
+ descriptor: payload.descriptor,
606
+ escrow: tip20ChannelEscrow,
607
+ requiredCumulative: '2000000',
608
+ settled: '0',
609
+ spent: '1000000',
610
+ units: 1,
611
+ },
612
+ },
613
+ }),
614
+ ),
615
+ )
616
+ }
617
+
618
+ if (payload?.action === 'topUp') return Promise.resolve(makeOkResponse())
619
+
620
+ if (payload?.action === 'voucher') {
621
+ return Promise.resolve(
622
+ new Response('paid', {
623
+ status: 200,
624
+ headers: {
625
+ 'Payment-Receipt': serializeSessionReceipt(
626
+ createSessionReceipt({
627
+ acceptedCumulative: 2_000_000n,
628
+ challengeId,
629
+ channelId: payload.channelId,
630
+ spent: 2_000_000n,
631
+ units: 2,
632
+ }),
633
+ ),
634
+ },
635
+ }),
636
+ )
637
+ }
638
+
639
+ return Promise.resolve(makeOkResponse())
640
+ })
641
+
642
+ const s = sessionManager({
643
+ account,
644
+ client,
645
+ fetch: mockFetch as typeof globalThis.fetch,
646
+ maxDeposit: '10',
647
+ })
648
+
649
+ const response = await s.fetch('https://api.example.com/data')
650
+
651
+ expect(response.status).toBe(200)
652
+ expect(await response.text()).toBe('paid')
653
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
654
+ expect(s.state).toMatchObject({
655
+ status: 'active',
656
+ acceptedCumulative: '2000000',
657
+ deposit: '2000000',
658
+ spent: '2000000',
659
+ units: 2,
660
+ })
661
+ })
662
+
663
+ test('preemptively top-ups before signing an HTTP voucher that exceeds deposit', async () => {
664
+ const postedPayloads: SessionCredentialPayload[] = []
665
+ let challengeCount = 0
666
+ const receipt = (payload: Extract<SessionCredentialPayload, { channelId: Hex }>) =>
667
+ new Response('paid', {
668
+ status: 200,
669
+ headers: {
670
+ 'Payment-Receipt': serializeSessionReceipt(
671
+ createSessionReceipt({
672
+ acceptedCumulative: BigInt(
673
+ 'cumulativeAmount' in payload ? payload.cumulativeAmount : '1000000',
674
+ ),
675
+ challengeId,
676
+ channelId: payload.channelId,
677
+ spent: BigInt('cumulativeAmount' in payload ? payload.cumulativeAmount : '1000000'),
678
+ units:
679
+ 'cumulativeAmount' in payload && payload.cumulativeAmount === '2000000' ? 2 : 1,
680
+ }),
681
+ ),
682
+ },
683
+ })
684
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
685
+ const authorization = new Headers(init?.headers).get('Authorization')
686
+ const payload = authorization
687
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
688
+ : undefined
689
+ if (payload) postedPayloads.push(payload)
690
+
691
+ if (!payload) {
692
+ challengeCount++
693
+ return Promise.resolve(
694
+ make402Response(
695
+ makeChallenge(challengeCount === 1 ? { suggestedDeposit: '1000000' } : {}),
696
+ ),
697
+ )
698
+ }
699
+ if (payload.action === 'topUp') return Promise.resolve(makeOkResponse())
700
+ if (payload.action === 'open' || payload.action === 'voucher')
701
+ return Promise.resolve(receipt(payload))
702
+ return Promise.resolve(makeOkResponse())
703
+ })
704
+
705
+ const s = sessionManager({
706
+ account,
707
+ client,
708
+ fetch: mockFetch as typeof globalThis.fetch,
709
+ maxDeposit: '10',
710
+ })
711
+
712
+ await s.fetch('https://api.example.com/data')
713
+ const response = await s.fetch('https://api.example.com/data')
714
+
715
+ expect(response.status).toBe(200)
716
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
717
+ expect(s.state).toMatchObject({
718
+ status: 'active',
719
+ acceptedCumulative: '2000000',
720
+ deposit: '2000000',
721
+ spent: '2000000',
722
+ units: 2,
723
+ })
724
+ })
725
+ })
726
+
727
+ describe('.topUp()', () => {
728
+ test('posts a precompile top-up credential for the active channel', async () => {
729
+ const postedPayloads: SessionCredentialPayload[] = []
730
+ let callCount = 0
731
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
732
+ callCount++
733
+ const authorization = new Headers(init?.headers).get('Authorization')
734
+ if (authorization) {
735
+ postedPayloads.push(
736
+ Credential.deserialize<SessionCredentialPayload>(authorization).payload,
737
+ )
738
+ }
739
+ if (callCount === 1)
740
+ return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '5000000' })))
741
+ return Promise.resolve(makeOkResponse())
742
+ })
743
+
744
+ const s = sessionManager({
745
+ account,
746
+ client,
747
+ fetch: mockFetch as typeof globalThis.fetch,
748
+ maxDeposit: '10',
749
+ })
750
+
751
+ await s.fetch('https://api.example.com/data')
752
+ expect(s.state).toMatchObject({
753
+ status: 'active',
754
+ acceptedCumulative: '1000000',
755
+ deposit: '5000000',
756
+ })
757
+ const receipt = await s.topUp('1')
758
+
759
+ expect(receipt).toBeUndefined()
760
+ expect(s.state).toMatchObject({
761
+ status: 'active',
762
+ acceptedCumulative: '1000000',
763
+ deposit: '6000000',
764
+ })
765
+ expect(postedPayloads[0]?.action).toBe('open')
766
+ expect(postedPayloads[1]?.action).toBe('topUp')
767
+ const openPayload = postedPayloads[0]
768
+ const topUpPayload = postedPayloads[1]
769
+ if (openPayload?.action !== 'open' || topUpPayload?.action !== 'topUp') {
770
+ throw new Error('expected open then top-up payloads')
771
+ }
772
+ expect(topUpPayload.channelId).toBe(openPayload.channelId)
773
+ expect(topUpPayload.descriptor).toEqual(openPayload.descriptor)
774
+ expect(topUpPayload.additionalDeposit).toBe('1000000')
775
+ })
776
+
777
+ test('rejects top-up before a channel is open', async () => {
778
+ const s = sessionManager({
779
+ account,
780
+ client,
781
+ fetch: vi.fn() as typeof globalThis.fetch,
782
+ maxDeposit: '10',
783
+ })
784
+
785
+ await expect(s.topUp('1')).rejects.toThrow('Cannot top up session: no open channel.')
786
+ })
787
+
788
+ test('rolls back optimistic vouchers when the paid retry fails', async () => {
789
+ const postedPayloads: SessionCredentialPayload[] = []
790
+ let callCount = 0
791
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
792
+ callCount++
793
+ const authorization = new Headers(init?.headers).get('Authorization')
794
+ if (authorization) {
795
+ postedPayloads.push(
796
+ Credential.deserialize<SessionCredentialPayload>(authorization).payload,
797
+ )
798
+ }
799
+ if (callCount === 1)
800
+ return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '3000000' })))
801
+ if (callCount === 3) return Promise.resolve(make402Response())
802
+ if (callCount === 4) return Promise.resolve(make402Response())
803
+ return Promise.resolve(makeOkResponse())
804
+ })
805
+
806
+ const s = sessionManager({
807
+ account,
808
+ client,
809
+ fetch: mockFetch as typeof globalThis.fetch,
810
+ maxDeposit: '3',
811
+ })
812
+
813
+ await s.fetch('https://api.example.com/data')
814
+ expect(s.cumulative).toBe(1000000n)
815
+
816
+ const failed = await s.fetch('https://api.example.com/data')
817
+ expect(failed.status).toBe(402)
818
+ expect(s.cumulative).toBe(1000000n)
819
+
820
+ await s.topUp('1')
821
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'voucher', 'topUp'])
822
+ const topUpPayload = postedPayloads[2]
823
+ if (topUpPayload?.action !== 'topUp') throw new Error('expected top-up payload')
824
+ expect(topUpPayload.additionalDeposit).toBe('1000000')
825
+ })
826
+ })
827
+
828
+ describe('.sse() event parsing', () => {
829
+ test('yields only message data from SSE stream', async () => {
830
+ const events = [
831
+ 'event: message\ndata: chunk1\n\n',
832
+ 'event: message\ndata: chunk2\n\n',
833
+ `event: payment-receipt\ndata: ${JSON.stringify({
834
+ method: 'tempo',
835
+ intent: 'session',
836
+ status: 'success',
837
+ timestamp: '2025-01-01T00:00:00.000Z',
838
+ reference: channelId,
839
+ challengeId,
840
+ channelId,
841
+ acceptedCumulative: '2000000',
842
+ spent: '2000000',
843
+ units: 2,
844
+ } satisfies SessionReceipt)}\n\n`,
845
+ ]
846
+
847
+ let callCount = 0
848
+ const mockFetch = vi.fn().mockImplementation(() => {
849
+ callCount++
850
+ if (callCount === 1) return Promise.resolve(makeSseResponse(events))
851
+ return Promise.resolve(makeOkResponse())
852
+ })
853
+
854
+ const s = sessionManager({
855
+ account: '0x0000000000000000000000000000000000000001',
856
+ fetch: mockFetch as typeof globalThis.fetch,
857
+ })
858
+
859
+ // Manually set channel state to skip auto-open flow
860
+ ;(s as any).__test_setChannel?.()
861
+
862
+ const receiptCb = vi.fn()
863
+ const iterable = await s.sse('https://api.example.com/stream', {
864
+ onReceipt: receiptCb,
865
+ })
866
+
867
+ const messages: string[] = []
868
+ for await (const msg of iterable) {
869
+ messages.push(msg)
870
+ }
871
+
872
+ expect(messages).toEqual(['chunk1', 'chunk2'])
873
+ expect(receiptCb).toHaveBeenCalledOnce()
874
+ expect(receiptCb.mock.calls[0]![0].units).toBe(2)
875
+ })
876
+
877
+ test('posts precompile SSE top-up vouchers with the channel descriptor', async () => {
878
+ const requestedUrls: string[] = []
879
+ const postedPayloads: SessionCredentialPayload[] = []
880
+ let callCount = 0
881
+ const mockFetch = vi.fn().mockImplementation((input, init?: RequestInit) => {
882
+ callCount++
883
+ requestedUrls.push(input.toString())
884
+ const authorization = new Headers(init?.headers).get('Authorization')
885
+ if (authorization) {
886
+ postedPayloads.push(
887
+ Credential.deserialize<SessionCredentialPayload>(authorization).payload,
888
+ )
889
+ }
890
+ if (callCount === 1) return Promise.resolve(make402Response())
891
+ if (callCount === 2) {
892
+ const openPayload = postedPayloads[0]
893
+ if (openPayload?.action !== 'open') throw new Error('expected open payload')
894
+ const needVoucher: NeedVoucherEvent = {
895
+ channelId: openPayload.channelId,
896
+ requiredCumulative: '2000000',
897
+ acceptedCumulative: '1000000',
898
+ deposit: '10000000',
899
+ }
900
+ return Promise.resolve(
901
+ makeSseResponse([
902
+ 'event: message\ndata: chunk1\n\n',
903
+ formatNeedVoucherEvent(needVoucher),
904
+ 'event: message\ndata: chunk2\n\n',
905
+ ]),
906
+ )
907
+ }
908
+ return Promise.resolve(makeOkResponse())
909
+ })
910
+
911
+ const s = sessionManager({
912
+ account,
913
+ client,
914
+ fetch: mockFetch as typeof globalThis.fetch,
915
+ maxDeposit: '10',
916
+ })
917
+
918
+ const iterable = await s.sse('https://api.example.com/stream?prompt=paid')
919
+ const messages: string[] = []
920
+ for await (const msg of iterable) messages.push(msg)
921
+
922
+ expect(messages).toEqual(['chunk1', 'chunk2'])
923
+ expect(postedPayloads[0]?.action).toBe('open')
924
+ expect(postedPayloads[1]?.action).toBe('voucher')
925
+ const openPayload = postedPayloads[0]
926
+ const voucherPayload = postedPayloads[1]
927
+ if (openPayload?.action !== 'open' || voucherPayload?.action !== 'voucher')
928
+ throw new Error('expected open then voucher payloads')
929
+ expect(voucherPayload.channelId).toBe(openPayload.channelId)
930
+ expect(voucherPayload.descriptor).toEqual(openPayload.descriptor)
931
+ expect(voucherPayload.cumulativeAmount).toBe('2000000')
932
+ expect(requestedUrls[2]).toBe('https://api.example.com/stream')
933
+ })
934
+
935
+ test('ignores precompile SSE voucher requests for a different channel', async () => {
936
+ const mismatchedChannelId = `0x${'ff'.repeat(32)}` as Hex
937
+ const needVoucher: NeedVoucherEvent = {
938
+ channelId: mismatchedChannelId,
939
+ requiredCumulative: '2000000',
940
+ acceptedCumulative: '1000000',
941
+ deposit: '10000000',
942
+ }
943
+ const events = [
944
+ 'event: message\ndata: chunk1\n\n',
945
+ formatNeedVoucherEvent(needVoucher),
946
+ 'event: message\ndata: chunk2\n\n',
947
+ ]
948
+ const postedPayloads: SessionCredentialPayload[] = []
949
+ let callCount = 0
950
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
951
+ callCount++
952
+ const authorization = new Headers(init?.headers).get('Authorization')
953
+ if (authorization) {
954
+ postedPayloads.push(
955
+ Credential.deserialize<SessionCredentialPayload>(authorization).payload,
956
+ )
957
+ }
958
+ if (callCount === 1) return Promise.resolve(make402Response())
959
+ if (callCount === 2) return Promise.resolve(makeSseResponse(events))
960
+ return Promise.resolve(makeOkResponse())
961
+ })
962
+
963
+ const s = sessionManager({
964
+ account,
965
+ client,
966
+ fetch: mockFetch as typeof globalThis.fetch,
967
+ maxDeposit: '10',
968
+ })
969
+
970
+ const iterable = await s.sse('https://api.example.com/stream')
971
+ const messages: string[] = []
972
+ for await (const msg of iterable) messages.push(msg)
973
+
974
+ expect(messages).toEqual(['chunk1', 'chunk2'])
975
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open'])
976
+ expect(callCount).toBe(2)
977
+ })
978
+
979
+ test('retries for the event stream after an open management response', async () => {
980
+ const postedPayloads: SessionCredentialPayload[] = []
981
+ let callCount = 0
982
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
983
+ callCount++
984
+ const authorization = new Headers(init?.headers).get('Authorization')
985
+ const payload = authorization
986
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
987
+ : undefined
988
+ if (payload) postedPayloads.push(payload)
989
+
990
+ if (!payload) {
991
+ return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '5000000' })))
992
+ }
993
+
994
+ if (payload.action === 'open') {
995
+ return Promise.resolve(
996
+ new Response(null, {
997
+ status: 200,
998
+ headers: {
999
+ 'Payment-Receipt': serializeSessionReceipt(
1000
+ createSessionReceipt({
1001
+ acceptedCumulative: 1_000_000n,
1002
+ challengeId,
1003
+ channelId: payload.channelId,
1004
+ spent: 1_000_000n,
1005
+ units: 1,
1006
+ }),
1007
+ ),
1008
+ },
1009
+ }),
1010
+ )
1011
+ }
1012
+
1013
+ if (payload.action === 'voucher') {
1014
+ return Promise.resolve(
1015
+ makeSseResponse([
1016
+ 'event: message\ndata: chunk1\n\n',
1017
+ 'event: message\ndata: chunk2\n\n',
1018
+ `event: payment-receipt\ndata: ${JSON.stringify(
1019
+ createSessionReceipt({
1020
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
1021
+ challengeId,
1022
+ channelId: payload.channelId,
1023
+ spent: BigInt(payload.cumulativeAmount),
1024
+ units: 2,
1025
+ }),
1026
+ )}\n\n`,
1027
+ ]),
1028
+ )
1029
+ }
1030
+
1031
+ return Promise.resolve(makeOkResponse())
1032
+ })
1033
+
1034
+ const s = sessionManager({
1035
+ account,
1036
+ client,
1037
+ fetch: mockFetch as typeof globalThis.fetch,
1038
+ maxDeposit: '10',
1039
+ })
1040
+
1041
+ const iterable = await s.sse('https://api.example.com/stream')
1042
+ const messages: string[] = []
1043
+ for await (const msg of iterable) messages.push(msg)
1044
+
1045
+ expect(messages).toEqual(['chunk1', 'chunk2'])
1046
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'voucher'])
1047
+ expect(callCount).toBe(4)
1048
+ expect(s.state).toMatchObject({
1049
+ status: 'active',
1050
+ acceptedCumulative: '2000000',
1051
+ deposit: '5000000',
1052
+ spent: '2000000',
1053
+ units: 2,
1054
+ })
1055
+ })
1056
+ })
1057
+
1058
+ describe('error handling', () => {
1059
+ test('.sse() silently skips payment-need-voucher when no channel open', async () => {
1060
+ const needVoucher: NeedVoucherEvent = {
1061
+ channelId,
1062
+ requiredCumulative: '2000000',
1063
+ acceptedCumulative: '1000000',
1064
+ deposit: '10000000',
1065
+ }
1066
+
1067
+ const events = [
1068
+ 'event: message\ndata: chunk1\n\n',
1069
+ formatNeedVoucherEvent(needVoucher),
1070
+ 'event: message\ndata: chunk2\n\n',
1071
+ ]
1072
+
1073
+ const mockFetch = vi.fn().mockResolvedValue(makeSseResponse(events))
1074
+
1075
+ const s = sessionManager({
1076
+ account: '0x0000000000000000000000000000000000000001',
1077
+ fetch: mockFetch as typeof globalThis.fetch,
1078
+ })
1079
+
1080
+ const iterable = await s.sse('https://api.example.com/stream')
1081
+
1082
+ const messages: string[] = []
1083
+ for await (const msg of iterable) {
1084
+ messages.push(msg)
1085
+ }
1086
+
1087
+ expect(messages).toEqual(['chunk1', 'chunk2'])
1088
+ expect(mockFetch).toHaveBeenCalledOnce()
1089
+ })
1090
+ })
1091
+
1092
+ describe('.sse() headers normalization', () => {
1093
+ test('preserves Headers instance properties when passed as headers', async () => {
1094
+ const mockFetch = vi.fn().mockResolvedValue(makeSseResponse(['event: message\ndata: ok\n\n']))
1095
+
1096
+ const s = sessionManager({
1097
+ account: '0x0000000000000000000000000000000000000001',
1098
+ fetch: mockFetch as typeof globalThis.fetch,
1099
+ })
1100
+
1101
+ const iterable = await s.sse('https://api.example.com/stream', {
1102
+ headers: new Headers({ 'Content-Type': 'application/json', 'X-Custom': 'value' }),
1103
+ })
1104
+
1105
+ for await (const _ of iterable) {
1106
+ // drain
1107
+ }
1108
+
1109
+ const calledHeaders = new Headers((mockFetch.mock.calls[0]![1] as RequestInit).headers)
1110
+ expect(calledHeaders.get('content-type')).toBe('application/json')
1111
+ expect(calledHeaders.get('x-custom')).toBe('value')
1112
+ expect(calledHeaders.get('accept')).toBe('text/event-stream')
1113
+ })
1114
+ })
1115
+
1116
+ describe('.close()', () => {
1117
+ test('is no-op when not opened', async () => {
1118
+ const mockFetch = vi.fn()
1119
+
1120
+ const s = sessionManager({
1121
+ account: '0x0000000000000000000000000000000000000001',
1122
+ fetch: mockFetch as typeof globalThis.fetch,
1123
+ })
1124
+
1125
+ await s.close()
1126
+ expect(mockFetch).not.toHaveBeenCalled()
1127
+ })
1128
+
1129
+ test('tracks spent from HTTP error receipts and closes at that amount', async () => {
1130
+ const postedPayloads: SessionCredentialPayload[] = []
1131
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
1132
+ const authorization = new Headers(init?.headers).get('Authorization')
1133
+ const payload = authorization
1134
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
1135
+ : undefined
1136
+ if (payload) postedPayloads.push(payload)
1137
+
1138
+ if (!payload)
1139
+ return Promise.resolve(
1140
+ make402Response(makeChallenge({ amount: '1', suggestedDeposit: '2' })),
1141
+ )
1142
+ if (payload.action === 'open') {
1143
+ return Promise.resolve(
1144
+ new Response('upstream failed', {
1145
+ status: 500,
1146
+ headers: {
1147
+ 'Payment-Receipt': serializeSessionReceipt(
1148
+ createSessionReceipt({
1149
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
1150
+ challengeId,
1151
+ channelId: payload.channelId,
1152
+ spent: 1n,
1153
+ units: 1,
1154
+ }),
1155
+ ),
1156
+ },
1157
+ }),
1158
+ )
1159
+ }
1160
+ if (payload.action === 'close') {
1161
+ return Promise.resolve(
1162
+ new Response(null, {
1163
+ status: 200,
1164
+ headers: {
1165
+ 'Payment-Receipt': serializeSessionReceipt(
1166
+ createSessionReceipt({
1167
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
1168
+ challengeId,
1169
+ channelId: payload.channelId,
1170
+ spent: BigInt(payload.cumulativeAmount),
1171
+ units: 1,
1172
+ }),
1173
+ ),
1174
+ },
1175
+ }),
1176
+ )
1177
+ }
1178
+ return Promise.resolve(makeOkResponse())
1179
+ })
1180
+
1181
+ const s = sessionManager({
1182
+ account,
1183
+ client,
1184
+ decimals: 0,
1185
+ fetch: mockFetch as typeof globalThis.fetch,
1186
+ maxDeposit: '2',
1187
+ })
1188
+
1189
+ const response = await s.fetch('https://api.example.com/resource')
1190
+ expect(response.status).toBe(500)
1191
+ expect(response.receipt?.spent).toBe('1')
1192
+
1193
+ const closeReceipt = await s.close()
1194
+ expect(closeReceipt?.status).toBe('success')
1195
+ expect(closeReceipt?.spent).toBe('1')
1196
+ expect(s.state).toMatchObject({
1197
+ status: 'closed',
1198
+ channelId: closeReceipt?.channelId,
1199
+ })
1200
+ const closePayload = postedPayloads.find((payload) => payload.action === 'close')
1201
+ expect(closePayload?.cumulativeAmount).toBe('1')
1202
+ })
1203
+
1204
+ test('rejects receipts that exceed the locally signed voucher', async () => {
1205
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
1206
+ const authorization = new Headers(init?.headers).get('Authorization')
1207
+ const payload = authorization
1208
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
1209
+ : undefined
1210
+
1211
+ if (!payload)
1212
+ return Promise.resolve(
1213
+ make402Response(makeChallenge({ amount: '1', suggestedDeposit: '3' })),
1214
+ )
1215
+ if (payload.action === 'open') {
1216
+ return Promise.resolve(
1217
+ new Response('ok', {
1218
+ status: 200,
1219
+ headers: {
1220
+ 'Payment-Receipt': serializeSessionReceipt(
1221
+ createSessionReceipt({
1222
+ acceptedCumulative: 3n,
1223
+ challengeId,
1224
+ channelId: payload.channelId,
1225
+ spent: 3n,
1226
+ units: 1,
1227
+ }),
1228
+ ),
1229
+ },
1230
+ }),
1231
+ )
1232
+ }
1233
+ return Promise.resolve(makeOkResponse())
1234
+ })
1235
+
1236
+ const s = sessionManager({
1237
+ account,
1238
+ client,
1239
+ decimals: 0,
1240
+ fetch: mockFetch as typeof globalThis.fetch,
1241
+ maxDeposit: '3',
1242
+ })
1243
+
1244
+ await expect(s.fetch('https://api.example.com/resource')).rejects.toThrow(
1245
+ 'receipt accepted cumulative exceeds local voucher state',
1246
+ )
1247
+ })
1248
+
1249
+ test('surfaces close failure problem details', async () => {
1250
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
1251
+ const authorization = new Headers(init?.headers).get('Authorization')
1252
+ const payload = authorization
1253
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
1254
+ : undefined
1255
+
1256
+ if (!payload)
1257
+ return Promise.resolve(
1258
+ make402Response(makeChallenge({ amount: '1', suggestedDeposit: '2' })),
1259
+ )
1260
+ if (payload.action === 'open') {
1261
+ return Promise.resolve(
1262
+ new Response('ok', {
1263
+ status: 200,
1264
+ headers: {
1265
+ 'Payment-Receipt': serializeSessionReceipt(
1266
+ createSessionReceipt({
1267
+ acceptedCumulative: BigInt(payload.cumulativeAmount),
1268
+ challengeId,
1269
+ channelId: payload.channelId,
1270
+ spent: 1n,
1271
+ units: 1,
1272
+ }),
1273
+ ),
1274
+ },
1275
+ }),
1276
+ )
1277
+ }
1278
+ if (payload.action === 'close') {
1279
+ return Promise.resolve(
1280
+ new Response('close failed', {
1281
+ status: 500,
1282
+ headers: {
1283
+ 'WWW-Authenticate': 'Payment error="close_failed"',
1284
+ },
1285
+ }),
1286
+ )
1287
+ }
1288
+ return Promise.resolve(makeOkResponse())
1289
+ })
1290
+
1291
+ const s = sessionManager({
1292
+ account,
1293
+ client,
1294
+ decimals: 0,
1295
+ fetch: mockFetch as typeof globalThis.fetch,
1296
+ maxDeposit: '2',
1297
+ })
1298
+
1299
+ const response = await s.fetch('https://api.example.com/resource')
1300
+ expect(response.status).toBe(200)
1301
+ expect(response.receipt?.spent).toBe('1')
1302
+
1303
+ await expect(s.close()).rejects.toThrow(
1304
+ 'Close request failed with status 500: close failed [WWW-Authenticate: Payment error="close_failed"]',
1305
+ )
1306
+ })
1307
+ })
1308
+ })