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,1292 @@
1
+ import type { Hex } from 'ox'
2
+
3
+ import * as Challenge from '../../../Challenge.js'
4
+ import * as Fetch from '../../../client/internal/Fetch.js'
5
+ import * as Constants from '../../../Constants.js'
6
+ import * as PaymentCredential from '../../../Credential.js'
7
+ import * as z from '../../../zod.js'
8
+ import * as Methods from '../../Methods.js'
9
+ import {
10
+ deserializeSessionReceipt,
11
+ isEventStream,
12
+ parseEvent,
13
+ readSessionChallengeAmount,
14
+ uint96,
15
+ type NeedVoucherEvent,
16
+ type SessionCredentialPayload,
17
+ type SessionReceipt,
18
+ } from '../precompile/Protocol.js'
19
+ import * as Ws from '../precompile/Protocol.js'
20
+ import type { SessionSnapshot } from '../Snapshot.js'
21
+ import type { ChannelEntry } from './ChannelOps.js'
22
+ import type { SessionContext } from './CredentialState.js'
23
+ import {
24
+ activeStateFromChannel,
25
+ activeStateFromReceipt,
26
+ isExpectedCloseReceipt,
27
+ parseManagerAmount,
28
+ type CloseTarget,
29
+ type SessionState,
30
+ } from './Runtime.js'
31
+
32
+ /** Runtime WebSocket constructor accepted by `sessionManager()` in non-browser environments. */
33
+ export type WebSocketConstructor = {
34
+ new (url: string | URL, protocols?: string | string[]): WebSocket
35
+ }
36
+
37
+ /** Numeric ready-state constants used by browser-compatible WebSocket clients. */
38
+ export const WebSocketReadyState = {
39
+ CONNECTING: 0,
40
+ OPEN: 1,
41
+ CLOSING: 2,
42
+ CLOSED: 3,
43
+ } as const
44
+
45
+ // Browser-style WebSocket clients may only initiate close with 1000 or 3000-4999.
46
+ // Keep protocol/policy close codes on the server side and use an app-defined code here.
47
+ /** Client-side close code used for payment protocol errors. */
48
+ export const ClientWebSocketProtocolErrorCloseCode = 3008
49
+
50
+ type EventType = 'close' | 'error' | 'message' | 'open'
51
+ type ManagedEventMap = {
52
+ close: { code: number; reason: string; type: 'close'; wasClean: boolean }
53
+ error: { type: 'error' }
54
+ message: { data: string; type: 'message' }
55
+ open: { type: 'open' }
56
+ }
57
+ type ListenerValue<type extends EventType = EventType> =
58
+ | ((event: ManagedEventMap[type]) => void)
59
+ | { handleEvent(event: ManagedEventMap[type]): void }
60
+ type Listener = {
61
+ once: boolean
62
+ value: ListenerValue<EventType>
63
+ }
64
+ type ManagedSocketShape = {
65
+ onclose: ((event: ManagedEventMap['close']) => void) | null
66
+ onerror: ((event: ManagedEventMap['error']) => void) | null
67
+ onmessage: ((event: ManagedEventMap['message']) => void) | null
68
+ onopen: ((event: ManagedEventMap['open']) => void) | null
69
+ }
70
+
71
+ /** Managed socket facade returned to callers after payment protocol frames are intercepted. */
72
+ export type SessionManagedWebSocket = ManagedSocketShape & {
73
+ addEventListener<type extends EventType>(
74
+ type: type,
75
+ listener: ListenerValue<type>,
76
+ options?: boolean | AddEventListenerOptions,
77
+ ): void
78
+ readonly bufferedAmount: WebSocket['bufferedAmount']
79
+ close(code?: number, reason?: string): void
80
+ readonly extensions: WebSocket['extensions']
81
+ on<type extends EventType>(type: type, listener: (event: ManagedEventMap[type]) => void): void
82
+ off<type extends EventType>(type: type, listener: (event: ManagedEventMap[type]) => void): void
83
+ readonly protocol: WebSocket['protocol']
84
+ readonly readyState: WebSocket['readyState']
85
+ removeEventListener<type extends EventType>(type: type, listener: ListenerValue<type>): void
86
+ send(data: string): void
87
+ readonly url: WebSocket['url']
88
+ }
89
+
90
+ /** Wraps a raw WebSocket so protocol frames can be handled before user listeners see messages. */
91
+ export function createManagedSocket(socket: WebSocket) {
92
+ const listeners = new Map<EventType, Set<Listener>>()
93
+ let emittedClose = false
94
+ let messageBuffer: ManagedEventMap['message'][] | null = []
95
+ let readyState = socket.readyState
96
+
97
+ const add = <type extends EventType>(
98
+ type: type,
99
+ listener: ListenerValue<type>,
100
+ options?: boolean | AddEventListenerOptions,
101
+ ) => {
102
+ let set = listeners.get(type)
103
+ if (!set) {
104
+ set = new Set()
105
+ listeners.set(type, set)
106
+ }
107
+ set.add({
108
+ once: typeof options === 'object' ? options.once === true : false,
109
+ value: listener as ListenerValue<EventType>,
110
+ })
111
+ if (type === 'message' && messageBuffer) {
112
+ const buffered = messageBuffer
113
+ messageBuffer = null
114
+ for (const event of buffered) emit('message', event)
115
+ }
116
+ }
117
+
118
+ const remove = <type extends EventType>(type: type, listener: ListenerValue<type>) => {
119
+ const set = listeners.get(type)
120
+ if (!set) return
121
+ for (const entry of set) {
122
+ if (entry.value === listener) set.delete(entry)
123
+ }
124
+ }
125
+
126
+ function emit<type extends EventType>(type: type, event: ManagedEventMap[type]) {
127
+ if (event.type === 'close') {
128
+ if (emittedClose) return
129
+ emittedClose = true
130
+ readyState = WebSocketReadyState.CLOSED
131
+ messageBuffer = null
132
+ }
133
+ if (event.type === 'open') readyState = WebSocketReadyState.OPEN
134
+
135
+ if (event.type === 'message' && messageBuffer) {
136
+ messageBuffer.push(event)
137
+ return
138
+ }
139
+
140
+ switch (event.type) {
141
+ case 'close':
142
+ managed.onclose?.(event)
143
+ break
144
+ case 'error':
145
+ managed.onerror?.(event)
146
+ break
147
+ case 'message':
148
+ managed.onmessage?.(event)
149
+ break
150
+ case 'open':
151
+ managed.onopen?.(event)
152
+ break
153
+ }
154
+
155
+ const set = listeners.get(type)
156
+ if (!set) return
157
+ for (const entry of Array.from(set)) {
158
+ if (typeof entry.value === 'function') entry.value(event)
159
+ else entry.value.handleEvent(event)
160
+ if (entry.once) set.delete(entry)
161
+ }
162
+ }
163
+
164
+ let onmessage: ManagedSocketShape['onmessage'] = null
165
+ const managed: SessionManagedWebSocket = {
166
+ addEventListener: add,
167
+ close(code?: number, reason?: string) {
168
+ socket.close(code, reason)
169
+ },
170
+ get bufferedAmount() {
171
+ return socket.bufferedAmount
172
+ },
173
+ get extensions() {
174
+ return socket.extensions
175
+ },
176
+ on<type extends EventType>(type: type, listener: (event: ManagedEventMap[type]) => void) {
177
+ add(type, listener)
178
+ },
179
+ onclose: null as ManagedSocketShape['onclose'],
180
+ onerror: null as ManagedSocketShape['onerror'],
181
+ get onmessage() {
182
+ return onmessage
183
+ },
184
+ set onmessage(fn: ManagedSocketShape['onmessage']) {
185
+ onmessage = fn
186
+ if (fn && messageBuffer) {
187
+ const buffered = messageBuffer
188
+ messageBuffer = null
189
+ for (const event of buffered) emit('message', event)
190
+ }
191
+ },
192
+ onopen: null as ManagedSocketShape['onopen'],
193
+ off<type extends EventType>(type: type, listener: (event: ManagedEventMap[type]) => void) {
194
+ remove(type, listener)
195
+ },
196
+ get protocol() {
197
+ return socket.protocol
198
+ },
199
+ get readyState() {
200
+ return readyState
201
+ },
202
+ removeEventListener: remove,
203
+ send(data: string) {
204
+ socket.send(data)
205
+ },
206
+ get url() {
207
+ return socket.url
208
+ },
209
+ }
210
+
211
+ return {
212
+ emit,
213
+ socket: managed,
214
+ }
215
+ }
216
+
217
+ /** Top-up requirement emitted by HTTP, SSE, or WebSocket session flows. */
218
+ export type TopUpRequirement = {
219
+ /** Challenge used to authorize the management request. */
220
+ challenge: TempoSessionChallenge
221
+ /** Channel that requires more deposit. */
222
+ channelId: Hex.Hex
223
+ /** Current channel deposit in raw units. */
224
+ deposit: bigint
225
+ /** Original paid resource URL; converted to a management URL by the caller. */
226
+ input: RequestInfo | URL
227
+ /** Minimum cumulative voucher amount the server needs. */
228
+ requiredCumulative: bigint
229
+ }
230
+
231
+ /** Inputs needed to validate a caller-requested manager top-up. */
232
+ export type ResolveManualTopUpParameters = {
233
+ /** Human-readable or raw top-up amount passed to `SessionManager.topUp()`. */
234
+ amount: string | bigint
235
+ /** Local policy guard for cumulative voucher authorization. */
236
+ assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
237
+ /** Active channel cache entry, when one exists. */
238
+ channel: ChannelEntry | null
239
+ /** Token decimals used to parse string amounts. */
240
+ decimals: number
241
+ /** Last challenge observed by the manager. */
242
+ lastChallenge: TempoSessionChallenge | null
243
+ /** Last paid resource or management URL available to the manager. */
244
+ lastUrl: RequestInfo | URL | null
245
+ }
246
+
247
+ /** Validated target for a caller-requested manager top-up. */
248
+ export type ManualTopUpTarget = {
249
+ /** Additional deposit in raw token units. */
250
+ additionalDeposit: bigint
251
+ /** Challenge used to authorize the top-up credential. */
252
+ challenge: TempoSessionChallenge
253
+ /** Channel ID receiving the top-up. */
254
+ channelId: Hex.Hex
255
+ /** URL used for the top-up management POST. */
256
+ input: RequestInfo | URL
257
+ }
258
+
259
+ /** Inputs for applying a successful top-up POST to local manager state. */
260
+ export type ApplyTopUpResultParameters = {
261
+ /** Additional deposit accepted by the top-up credential. */
262
+ additionalDeposit: bigint
263
+ /** Active local channel cache entry, when one exists. */
264
+ channel: ChannelEntry | null
265
+ /** Channel ID targeted by the top-up. */
266
+ channelId: Hex.Hex
267
+ /** Current active challenge ID, used when the server did not return a receipt. */
268
+ challengeId?: string | undefined
269
+ /** Current active machine state, used to preserve paid unit count when possible. */
270
+ currentState: SessionState
271
+ /** Receipt returned by the top-up POST, when present. */
272
+ receipt?: SessionReceipt | undefined
273
+ /** Latest locally observed spend in raw units. */
274
+ spent: bigint
275
+ }
276
+
277
+ /** Local runtime updates produced by a top-up POST. */
278
+ export type AppliedTopUpResult = {
279
+ /** Channel with updated deposit. */
280
+ channel: ChannelEntry
281
+ /** Next public machine state, when enough context is available to project one. */
282
+ state?: SessionState | undefined
283
+ }
284
+
285
+ /** Parsed raw-unit amounts from a server need-voucher event. */
286
+ export type NeedVoucherEventAmounts = {
287
+ /** Highest voucher amount currently accepted by the server. */
288
+ acceptedCumulative: bigint
289
+ /** Current on-chain deposit reported by the server. */
290
+ deposit: bigint
291
+ /** Minimum cumulative voucher amount required by the server. */
292
+ requiredCumulative: bigint
293
+ }
294
+
295
+ /** Inputs used to satisfy a server need-voucher event. */
296
+ export type ResolveNeedVoucherContextParameters = {
297
+ /** Local policy guard for cumulative voucher authorization. */
298
+ assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
299
+ /** Challenge used for follow-up management credentials. */
300
+ challenge: TempoSessionChallenge
301
+ /** Server need-voucher event. */
302
+ event: NeedVoucherEvent
303
+ /** Expected channel ID for this transport flow. */
304
+ expectedChannelId: Hex.Hex
305
+ /** Reads the latest active channel after any top-up. */
306
+ getChannel(): ChannelEntry | null
307
+ /** Original paid resource URL; converted to management URL by the caller. */
308
+ input: RequestInfo | URL
309
+ /** Performs the deposit top-up when server-required cumulative exceeds deposit. */
310
+ topUpIfNeeded(parameters: TopUpRequirement): Promise<void>
311
+ }
312
+
313
+ /** Result of handling a server need-voucher event. */
314
+ export type NeedVoucherResolution =
315
+ | {
316
+ /** A voucher credential can be signed with this context. */
317
+ status: 'ready'
318
+ /** Context passed to the low-level session credential method. */
319
+ context: SessionContext
320
+ }
321
+ | {
322
+ /** No voucher should be sent for this event. */
323
+ status: 'ignored'
324
+ /** Why the event was ignored. */
325
+ reason: 'channel-mismatch' | 'missing-channel'
326
+ }
327
+
328
+ /** Parses raw-unit numeric fields from a need-voucher event. */
329
+ export function readNeedVoucherEventAmounts(event: NeedVoucherEvent): NeedVoucherEventAmounts {
330
+ return {
331
+ acceptedCumulative: BigInt(event.acceptedCumulative),
332
+ deposit: BigInt(event.deposit),
333
+ requiredCumulative: BigInt(event.requiredCumulative),
334
+ }
335
+ }
336
+
337
+ /** Validates local manager state and returns the concrete top-up operation to execute. */
338
+ export function resolveManualTopUp(parameters: ResolveManualTopUpParameters): ManualTopUpTarget {
339
+ const { amount, assertVoucherWithinLocalLimit, channel, decimals, lastChallenge, lastUrl } =
340
+ parameters
341
+
342
+ if (!channel?.opened) throw new Error('Cannot top up session: no open channel.')
343
+ if (!lastChallenge) {
344
+ throw new Error('Cannot top up session: no challenge available. Make a request first.')
345
+ }
346
+ if (!lastUrl) {
347
+ throw new Error('Cannot top up session: no URL available. Call fetch(), sse(), or ws() first.')
348
+ }
349
+
350
+ const additionalDeposit = parseManagerAmount(amount, decimals)
351
+ if (additionalDeposit <= 0n) throw new Error('Top-up amount must be greater than zero.')
352
+ assertVoucherWithinLocalLimit(channel.cumulativeAmount + additionalDeposit)
353
+
354
+ return {
355
+ additionalDeposit,
356
+ challenge: lastChallenge,
357
+ channelId: channel.channelId,
358
+ input: lastUrl,
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Applies local deposit and state bookkeeping for a top-up response.
364
+ *
365
+ * `deposit` is updated from the top-up amount. Receipt cumulative values only
366
+ * update accepted spend authorization; they do not replace deposit.
367
+ */
368
+ export function applyTopUpResult(
369
+ parameters: ApplyTopUpResultParameters,
370
+ ): AppliedTopUpResult | undefined {
371
+ const { additionalDeposit, channel, channelId, challengeId, currentState, receipt, spent } =
372
+ parameters
373
+ if (channel?.channelId !== channelId) return undefined
374
+
375
+ channel.deposit += additionalDeposit
376
+
377
+ if (receipt) return { channel, state: activeStateFromReceipt(receipt, channel) }
378
+ if (!challengeId) return { channel }
379
+
380
+ return {
381
+ channel,
382
+ state: activeStateFromChannel({
383
+ challengeId,
384
+ entry: channel,
385
+ spent: spent.toString(),
386
+ units: currentState.status === 'active' ? currentState.units : 0,
387
+ }),
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Applies local top-up/cumulative bookkeeping for a need-voucher event and
393
+ * returns an explicit transport decision.
394
+ */
395
+ export async function resolveNeedVoucherContext(
396
+ parameters: ResolveNeedVoucherContextParameters,
397
+ ): Promise<NeedVoucherResolution> {
398
+ if (parameters.event.channelId !== parameters.expectedChannelId) {
399
+ return { status: 'ignored', reason: 'channel-mismatch' }
400
+ }
401
+
402
+ const eventAmounts = readNeedVoucherEventAmounts(parameters.event)
403
+ parameters.assertVoucherWithinLocalLimit(eventAmounts.requiredCumulative)
404
+
405
+ await parameters.topUpIfNeeded({
406
+ challenge: parameters.challenge,
407
+ input: parameters.input,
408
+ channelId: parameters.expectedChannelId,
409
+ deposit: eventAmounts.deposit,
410
+ requiredCumulative: eventAmounts.requiredCumulative,
411
+ })
412
+
413
+ const channel = parameters.getChannel()
414
+ if (!channel || channel.channelId !== parameters.expectedChannelId) {
415
+ return { status: 'ignored', reason: 'missing-channel' }
416
+ }
417
+
418
+ const cumulativeAmount =
419
+ channel.cumulativeAmount > eventAmounts.requiredCumulative
420
+ ? channel.cumulativeAmount
421
+ : uint96(eventAmounts.requiredCumulative)
422
+
423
+ return {
424
+ status: 'ready',
425
+ context: {
426
+ action: 'voucher',
427
+ channelId: parameters.expectedChannelId,
428
+ descriptor: channel.descriptor,
429
+ cumulativeAmountRaw: cumulativeAmount.toString(),
430
+ },
431
+ }
432
+ }
433
+
434
+ /** Canonical challenge shape for built-in `tempo/session` requests. */
435
+ export type TempoSessionChallenge = Challenge.Challenge<
436
+ z.output<typeof Methods.session.schema.request>,
437
+ typeof Constants.Intents.session,
438
+ typeof Constants.Methods.tempo
439
+ >
440
+
441
+ /** Creates a credential bound to the current session challenge. */
442
+ export type CreateSessionCredential = (
443
+ challenge: TempoSessionChallenge,
444
+ context: SessionContext,
445
+ ) => Promise<string>
446
+
447
+ /** Inputs for posting a precompile session top-up credential. */
448
+ export type PostTopUpParameters = {
449
+ /** Additional deposit in raw token units. */
450
+ additionalDeposit: bigint
451
+ /** Challenge used to authorize the management request. */
452
+ challenge: TempoSessionChallenge
453
+ /** Local channel expected to receive the top-up. */
454
+ channel: ChannelEntry | null
455
+ /** Channel ID being topped up. */
456
+ channelId: Hex.Hex
457
+ /** Creates the signed top-up credential. */
458
+ createSessionCredential: CreateSessionCredential
459
+ /** Fetch implementation used for the management POST. */
460
+ fetch: typeof globalThis.fetch
461
+ /** Original paid resource URL; normalized to a management URL before posting. */
462
+ input: RequestInfo | URL
463
+ }
464
+
465
+ /** Inputs for retrying an HTTP 402 with a top-up/voucher management round trip. */
466
+ export type RetryHttpPaymentRequiredParameters = {
467
+ /** Creates the signed voucher credential. */
468
+ createSessionCredential: CreateSessionCredential
469
+ /** Fetch implementation used for the paid retry. */
470
+ fetch: typeof globalThis.fetch
471
+ /** Returns the current active channel after any top-up side effects. */
472
+ getChannel(): ChannelEntry | null
473
+ /** Original request init used by the paid resource request. */
474
+ init?: RequestInit | undefined
475
+ /** Original paid resource URL. */
476
+ input: RequestInfo | URL
477
+ /** Failed HTTP response that may contain a session challenge. */
478
+ response: Response
479
+ /** Restores local cumulative authorization if the voucher retry fails. */
480
+ restoreCumulative(channelId: Hex.Hex, cumulativeAmount: bigint): void
481
+ /** Stores the selected follow-up challenge on the manager. */
482
+ setChallenge(challenge: TempoSessionChallenge): void
483
+ /** Performs automatic top-up before the voucher retry when deposit is insufficient. */
484
+ topUpIfNeeded(parameters: TopUpRequirement): Promise<void>
485
+ }
486
+
487
+ /** Resolved data needed to perform an automatic HTTP voucher retry. */
488
+ export type RetryHttpPaymentContext = {
489
+ /** Channel active before the retry attempt. */
490
+ channel: ChannelEntry
491
+ /** Follow-up challenge selected from the 402 response. */
492
+ challenge: TempoSessionChallenge
493
+ /** Server snapshot describing the required voucher boundary. */
494
+ snapshot: NonNullable<ReturnType<typeof getSessionSnapshot>>
495
+ }
496
+
497
+ /** Inputs for posting a cooperative HTTP close credential. */
498
+ export type CloseHttpSessionParameters = {
499
+ /** Creates the signed close credential. */
500
+ createSessionCredential: CreateSessionCredential
501
+ /** Fetch implementation used for the close POST. */
502
+ fetch: typeof globalThis.fetch
503
+ /** Last paid resource URL; used as the management endpoint base. */
504
+ lastUrl: RequestInfo | URL | null
505
+ /** Final cumulative amount the client is willing to sign. */
506
+ signedCloseAmount: string
507
+ /** Stores a fresh close challenge when the first close credential expired. */
508
+ setChallenge?: ((challenge: TempoSessionChallenge) => void) | undefined
509
+ /** Channel/challenge pair being closed. */
510
+ target: CloseTarget
511
+ }
512
+
513
+ /** Returns true when a payment challenge is the built-in `tempo/session` method. */
514
+ export function isTempoSessionChallenge(
515
+ challenge: Challenge.Challenge,
516
+ ): challenge is TempoSessionChallenge {
517
+ return (
518
+ challenge.method === Constants.Methods.tempo && challenge.intent === Constants.Intents.session
519
+ )
520
+ }
521
+
522
+ /** Reads a server-provided session snapshot from challenge method details. */
523
+ export function getSessionSnapshot(challenge: TempoSessionChallenge): SessionSnapshot | undefined {
524
+ return Constants.getMethodDetail<SessionSnapshot>(
525
+ challenge.request.methodDetails,
526
+ Constants.MethodDetailKeys.sessionSnapshot,
527
+ )
528
+ }
529
+
530
+ /** Merges request headers and sets the payment authorization header for a retry. */
531
+ export function requestInitWithAuthorization(
532
+ input: RequestInfo | URL,
533
+ init: RequestInit | undefined,
534
+ credential: string,
535
+ ): RequestInit {
536
+ const requestHeaders = input instanceof Request ? input.headers : undefined
537
+ return {
538
+ ...init,
539
+ headers: {
540
+ ...Fetch.normalizeHeaders(requestHeaders),
541
+ ...Fetch.normalizeHeaders(init?.headers),
542
+ [Constants.Headers.authorization]: credential,
543
+ },
544
+ }
545
+ }
546
+
547
+ /** Returns the URL used for out-of-band management POSTs, stripping resource query state. */
548
+ export function managementInput(input: RequestInfo | URL): RequestInfo | URL {
549
+ try {
550
+ const base =
551
+ typeof location === 'undefined' ? undefined : (location.href as `${string}:${string}`)
552
+ const url =
553
+ input instanceof Request
554
+ ? new URL(input.url)
555
+ : input instanceof URL
556
+ ? new URL(input)
557
+ : new URL(String(input), base)
558
+ url.search = ''
559
+ return url
560
+ } catch {
561
+ return input
562
+ }
563
+ }
564
+
565
+ /** Converts a WebSocket URL into the HTTP URL used for its payment challenge probe. */
566
+ export function webSocketProbeUrl(input: string | URL): URL {
567
+ const url = new URL(input.toString())
568
+ if (url.protocol === 'ws:') url.protocol = 'http:'
569
+ if (url.protocol === 'wss:') url.protocol = 'https:'
570
+ return url
571
+ }
572
+
573
+ /** Reads an HTTP problem detail body, falling back to raw text for non-problem responses. */
574
+ export async function readProblemDetail(response: Response) {
575
+ const body = await response.text().catch(() => '')
576
+ if (!body) return ''
577
+ if (!response.headers.get('Content-Type')?.includes('application/problem+json')) return body
578
+ try {
579
+ const problem = JSON.parse(body) as { detail?: string }
580
+ return problem.detail ?? body
581
+ } catch {
582
+ return body
583
+ }
584
+ }
585
+
586
+ /** Posts a top-up management credential and returns its receipt, when present. */
587
+ export async function postTopUp(
588
+ parameters: PostTopUpParameters,
589
+ ): Promise<SessionReceipt | undefined> {
590
+ const { channel, channelId } = parameters
591
+ if (!channel?.descriptor || channel.channelId !== channelId) {
592
+ throw new Error('Cannot top up session: no local channel descriptor available.')
593
+ }
594
+
595
+ const credential = await parameters.createSessionCredential(parameters.challenge, {
596
+ action: 'topUp',
597
+ channelId,
598
+ descriptor: channel.descriptor,
599
+ additionalDepositRaw: parameters.additionalDeposit.toString(),
600
+ })
601
+ const response = await parameters.fetch(managementInput(parameters.input), {
602
+ method: 'POST',
603
+ headers: { [Constants.Headers.authorization]: credential },
604
+ })
605
+ if (!response.ok) throw new Error(`Top-up POST failed with status ${response.status}`)
606
+
607
+ const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt)
608
+ return receiptHeader ? deserializeSessionReceipt(receiptHeader) : undefined
609
+ }
610
+
611
+ /** Retries an HTTP 402 when the server snapshot asks for more voucher headroom. */
612
+ export async function retryHttpPaymentRequired(
613
+ parameters: RetryHttpPaymentRequiredParameters,
614
+ ): Promise<Response | undefined> {
615
+ const context = resolveRetryHttpPaymentContext({
616
+ channel: parameters.getChannel(),
617
+ response: parameters.response,
618
+ })
619
+ if (!context) return undefined
620
+ const { channel, challenge, snapshot } = context
621
+
622
+ parameters.setChallenge(challenge)
623
+ const requiredCumulative = BigInt(snapshot.requiredCumulative)
624
+ const cumulativeBeforeVoucher = channel.cumulativeAmount
625
+
626
+ await parameters.topUpIfNeeded({
627
+ challenge,
628
+ input: parameters.input,
629
+ channelId: snapshot.channelId,
630
+ deposit: BigInt(snapshot.deposit),
631
+ requiredCumulative,
632
+ })
633
+
634
+ const currentChannel = parameters.getChannel()
635
+ if (!currentChannel?.descriptor || currentChannel.channelId !== snapshot.channelId) {
636
+ return undefined
637
+ }
638
+ const cumulativeAmount =
639
+ currentChannel.cumulativeAmount > requiredCumulative
640
+ ? currentChannel.cumulativeAmount
641
+ : requiredCumulative
642
+ const credential = await parameters.createSessionCredential(challenge, {
643
+ action: 'voucher',
644
+ channelId: snapshot.channelId,
645
+ descriptor: currentChannel.descriptor,
646
+ cumulativeAmountRaw: cumulativeAmount.toString(),
647
+ })
648
+ const retry = await parameters.fetch(
649
+ parameters.input,
650
+ requestInitWithAuthorization(parameters.input, parameters.init, credential),
651
+ )
652
+ if (!retry.ok && !retry.headers.get(Constants.Headers.paymentReceipt)) {
653
+ parameters.restoreCumulative(snapshot.channelId, cumulativeBeforeVoucher)
654
+ }
655
+ return retry
656
+ }
657
+
658
+ /** Resolves whether a 402 response can be handled by an automatic session voucher retry. */
659
+ export function resolveRetryHttpPaymentContext(parameters: {
660
+ channel: ChannelEntry | null
661
+ response: Response
662
+ }): RetryHttpPaymentContext | undefined {
663
+ const challenge = Challenge.fromResponseList(parameters.response).find(isTempoSessionChallenge)
664
+ if (!challenge) return undefined
665
+
666
+ const snapshot = getSessionSnapshot(challenge)
667
+ if (!snapshot) return undefined
668
+
669
+ const { channel } = parameters
670
+ if (!channel?.descriptor || channel.channelId !== snapshot.channelId) return undefined
671
+
672
+ return { channel, challenge, snapshot }
673
+ }
674
+
675
+ /** Posts a cooperative close credential over HTTP and returns its receipt, when present. */
676
+ export async function closeHttpSession(
677
+ parameters: CloseHttpSessionParameters,
678
+ ): Promise<SessionReceipt | undefined> {
679
+ if (!parameters.lastUrl) {
680
+ throw new Error(
681
+ 'Cannot close session: no URL available. This usually means close() was called on a SessionManager instance that was recreated after the session was opened. Use the same SessionManager instance that opened the session, or call fetch()/sse() before close().',
682
+ )
683
+ }
684
+
685
+ const postClose = async (challenge: TempoSessionChallenge) => {
686
+ const credential = await parameters.createSessionCredential(challenge, {
687
+ action: 'close',
688
+ channelId: parameters.target.channelId,
689
+ descriptor: parameters.target.channel.descriptor,
690
+ cumulativeAmountRaw: parameters.signedCloseAmount,
691
+ })
692
+ return parameters.fetch(parameters.lastUrl!, {
693
+ method: 'POST',
694
+ headers: { [Constants.Headers.authorization]: credential },
695
+ })
696
+ }
697
+
698
+ let response = await postClose(parameters.target.challenge)
699
+ if (response.status === 402) {
700
+ const challenge = Challenge.fromResponseList(response).find(isTempoSessionChallenge)
701
+ if (challenge) {
702
+ parameters.setChallenge?.(challenge)
703
+ response = await postClose(challenge)
704
+ }
705
+ }
706
+ if (!response.ok) {
707
+ const detail = await readProblemDetail(response)
708
+ const wwwAuth = response.headers.get(Constants.Headers.wwwAuthenticate) ?? ''
709
+ throw new Error(
710
+ `Close request failed with status ${response.status}${detail ? `: ${detail}` : ''}${wwwAuth ? ` [${Constants.Headers.wwwAuthenticate}: ${wwwAuth}]` : ''}`,
711
+ )
712
+ }
713
+
714
+ const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt)
715
+ return receiptHeader ? deserializeSessionReceipt(receiptHeader) : undefined
716
+ }
717
+
718
+ /** Options accepted by the auto-driving SSE session flow. */
719
+ export type SseDriverOptions = RequestInit & {
720
+ /** Called for each payment receipt emitted by the SSE stream. */
721
+ onReceipt?: ((receipt: SessionReceipt) => void) | undefined
722
+ /** Abort signal used to cancel the stream. */
723
+ signal?: AbortSignal | undefined
724
+ }
725
+
726
+ /** Dependencies the SSE driver needs from `SessionManager`. */
727
+ export type OpenSseSessionParameters = {
728
+ /** Creates a session credential for the selected challenge/context. */
729
+ createSessionCredential(
730
+ challenge: TempoSessionChallenge,
731
+ context: SessionContext,
732
+ ): Promise<string>
733
+ /** Paid fetch flow used for the initial SSE request and any retry. */
734
+ doFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
735
+ /** Fetch implementation used for management voucher posts. */
736
+ fetch: typeof globalThis.fetch
737
+ /** Returns the active channel, if any. */
738
+ getChannel(): ChannelEntry | null
739
+ /** Returns the latest challenge observed by the manager. */
740
+ getChallenge(): TempoSessionChallenge | null
741
+ /** Validates a cumulative amount against local client policy. */
742
+ assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
743
+ /** Converts a resource URL to the server's session management URL. */
744
+ managementInput(input: RequestInfo | URL): RequestInfo | URL
745
+ /** Applies an incoming receipt to manager state. */
746
+ acceptReceipt(receipt: SessionReceipt): void
747
+ /** Performs an automatic channel top-up when deposit is insufficient. */
748
+ topUpIfNeeded(parameters: TopUpRequirement): Promise<void>
749
+ }
750
+
751
+ /**
752
+ * Opens an auto-driving paid SSE stream.
753
+ *
754
+ * The driver owns only transport parsing and management posts. Session state,
755
+ * credential creation, and top-up policy stay in `SessionManager`.
756
+ */
757
+ export async function openSseSession(
758
+ input: RequestInfo | URL,
759
+ init: SseDriverOptions | undefined,
760
+ driver: OpenSseSessionParameters,
761
+ ): Promise<AsyncIterable<string>> {
762
+ const { onReceipt, signal, ...fetchInit } = init ?? {}
763
+ const sseInit = {
764
+ ...fetchInit,
765
+ headers: {
766
+ ...Fetch.normalizeHeaders(fetchInit.headers),
767
+ Accept: 'text/event-stream',
768
+ },
769
+ ...(signal ? { signal } : {}),
770
+ }
771
+
772
+ let response = await driver.doFetch(input, sseInit)
773
+ if (!isEventStream(response) && driver.getChannel()?.opened)
774
+ response = await driver.doFetch(input, sseInit)
775
+
776
+ const challenge = driver.getChallenge()
777
+ if (!isEventStream(response)) throw new Error('SSE response is not an event stream.')
778
+ if (!response.body) throw new Error('Response has no body.')
779
+
780
+ return iterateSseResponse({
781
+ challenge,
782
+ driver,
783
+ input,
784
+ onReceipt,
785
+ response,
786
+ signal,
787
+ })
788
+ }
789
+
790
+ type IterateSseResponseParameters = {
791
+ challenge: TempoSessionChallenge | null
792
+ driver: OpenSseSessionParameters
793
+ input: RequestInfo | URL
794
+ onReceipt?: ((receipt: SessionReceipt) => void) | undefined
795
+ response: Response
796
+ signal?: AbortSignal | undefined
797
+ }
798
+
799
+ async function* iterateSseResponse(
800
+ parameters: IterateSseResponseParameters,
801
+ ): AsyncGenerator<string> {
802
+ const reader = parameters.response.body!.getReader()
803
+ const decoder = new TextDecoder()
804
+ let buffer = ''
805
+
806
+ try {
807
+ while (true) {
808
+ if (parameters.signal?.aborted) break
809
+
810
+ const { done, value } = await reader.read()
811
+ if (done) break
812
+
813
+ buffer += decoder.decode(value, { stream: true })
814
+ const parts = buffer.split('\n\n')
815
+ buffer = parts.pop()!
816
+
817
+ for (const part of parts) {
818
+ if (!part.trim()) continue
819
+ const event = parseEvent(part)
820
+ if (!event) continue
821
+
822
+ switch (event.type) {
823
+ case 'message':
824
+ yield event.data
825
+ break
826
+ case 'payment-need-voucher':
827
+ await handleSseNeedVoucher(parameters, event.data)
828
+ break
829
+ case 'payment-receipt':
830
+ parameters.driver.acceptReceipt(event.data)
831
+ parameters.onReceipt?.(event.data)
832
+ break
833
+ }
834
+ }
835
+ }
836
+ } finally {
837
+ reader.releaseLock()
838
+ }
839
+ }
840
+
841
+ async function handleSseNeedVoucher(
842
+ parameters: IterateSseResponseParameters,
843
+ event: NeedVoucherEvent,
844
+ ) {
845
+ const channel = parameters.driver.getChannel()
846
+ if (!channel || !parameters.challenge) return
847
+
848
+ const resolution = await resolveNeedVoucherContext({
849
+ assertVoucherWithinLocalLimit: parameters.driver.assertVoucherWithinLocalLimit,
850
+ challenge: parameters.challenge,
851
+ event,
852
+ expectedChannelId: channel.channelId,
853
+ getChannel: parameters.driver.getChannel,
854
+ input: parameters.input,
855
+ topUpIfNeeded: parameters.driver.topUpIfNeeded,
856
+ })
857
+ if (resolution.status !== 'ready') return
858
+
859
+ const credential = await parameters.driver.createSessionCredential(
860
+ parameters.challenge,
861
+ resolution.context,
862
+ )
863
+ const voucherResponse = await parameters.driver.fetch(
864
+ parameters.driver.managementInput(parameters.input),
865
+ {
866
+ method: 'POST',
867
+ headers: { [Constants.Headers.authorization]: credential },
868
+ },
869
+ )
870
+ if (!voucherResponse.ok) {
871
+ throw new Error(`Voucher POST failed with status ${voucherResponse.status}`)
872
+ }
873
+ }
874
+
875
+ /** Options accepted by the auto-driving WebSocket session flow. */
876
+ export type WebSocketDriverOptions = {
877
+ /** Called for each payment receipt emitted by the WebSocket flow. */
878
+ onReceipt?: ((receipt: SessionReceipt) => void) | undefined
879
+ /** WebSocket subprotocols passed to the runtime constructor. */
880
+ protocols?: string | string[] | undefined
881
+ /** Abort signal used to cancel the socket payment flow. */
882
+ signal?: AbortSignal | undefined
883
+ }
884
+
885
+ /** Creates a session credential for a WebSocket payment challenge. */
886
+ export type CreateWebSocketCredential = (
887
+ challenge: TempoSessionChallenge,
888
+ context: SessionContext,
889
+ ) => Promise<string>
890
+
891
+ /** Inputs for probing and authorizing a paid WebSocket session. */
892
+ export type PrepareWebSocketSessionParameters = {
893
+ /** Creates the opening session credential. */
894
+ createSessionCredential: CreateWebSocketCredential
895
+ /** Fetch implementation used for the HTTP 402 probe. */
896
+ fetch: typeof globalThis.fetch
897
+ /** WebSocket URL requested by the caller. */
898
+ input: string | URL
899
+ /** Called after resolving the HTTP probe URL, before the network request. */
900
+ onProbeUrl?: ((httpUrl: URL) => void) | undefined
901
+ /** Optional request init for the HTTP probe. */
902
+ probeInit?: RequestInit | undefined
903
+ /** Optional abort signal applied to the HTTP probe. */
904
+ signal?: AbortSignal | undefined
905
+ }
906
+
907
+ /** Result of the HTTP probe and opening credential creation for a WebSocket session. */
908
+ export type PreparedWebSocketSession = {
909
+ /** Selected tempo/session challenge from the HTTP probe. */
910
+ challenge: TempoSessionChallenge
911
+ /** Opening authorization credential to send in-band after the socket opens. */
912
+ credential: string
913
+ /** HTTP URL used for probe and out-of-band management requests. */
914
+ httpUrl: URL
915
+ /** WebSocket URL to open. */
916
+ wsUrl: URL
917
+ }
918
+
919
+ /** Active WebSocket session bookkeeping shared by the manager and socket driver. */
920
+ export type ActiveSocketSession = {
921
+ /** Challenge used to create credentials for this socket. */
922
+ challenge: TempoSessionChallenge
923
+ /** Channel authorized by the opening credential. */
924
+ channelId: Hex.Hex
925
+ /** Server close-ready receipt, when the stream is ready to close. */
926
+ closeReadyReceipt: SessionReceipt | null
927
+ /** Number of application chunks delivered through the managed socket. */
928
+ deliveredChunks: bigint
929
+ /** Expected final close amount while a close credential is in flight. */
930
+ expectedCloseAmount: string | null
931
+ /** Raw socket while open; set to null after close. */
932
+ socket: WebSocket | null
933
+ /** Raw token cost per delivered chunk. */
934
+ tickCost: bigint
935
+ }
936
+
937
+ /** Inputs for creating initial WebSocket payment runtime state. */
938
+ export type CreateActiveSocketSessionParameters = {
939
+ /** Challenge selected by the HTTP probe. */
940
+ challenge: TempoSessionChallenge
941
+ /** Opening credential sent when the socket opens. */
942
+ credential: string
943
+ /** Raw runtime socket. */
944
+ socket: WebSocket
945
+ }
946
+
947
+ /** Dependencies the WebSocket driver needs from `SessionManager`. */
948
+ export type OpenWebSocketSessionParameters = {
949
+ /** Challenge selected by the HTTP probe. */
950
+ challenge: TempoSessionChallenge
951
+ /** Opening credential to send in-band after the socket opens. */
952
+ credential: string
953
+ /** URL used for automatic top-up management calls. */
954
+ httpUrl: URL
955
+ /** WebSocket constructor for the current runtime. */
956
+ WebSocket: WebSocketConstructor
957
+ /** WebSocket URL to open. */
958
+ wsUrl: URL
959
+ /** Optional WebSocket call options. */
960
+ options?: WebSocketDriverOptions | undefined
961
+ /** Creates a session credential for the selected challenge/context. */
962
+ createSessionCredential(
963
+ challenge: TempoSessionChallenge,
964
+ context: SessionContext,
965
+ ): Promise<string>
966
+ /** Returns the active channel, if any. */
967
+ getChannel(): ChannelEntry | null
968
+ /** Stores the active socket state in the manager. */
969
+ setSocketSession(session: ActiveSocketSession): void
970
+ /** Validates a cumulative amount against local client policy. */
971
+ assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
972
+ /** Applies an incoming receipt to manager state. */
973
+ acceptReceipt(receipt: SessionReceipt): void
974
+ /** Rejects any pending close-ready wait. */
975
+ rejectCloseReady(error: Error): void
976
+ /** Rejects any pending receipt wait. */
977
+ rejectReceipt(error: Error): void
978
+ /** Records a close-ready receipt. */
979
+ settleCloseReady(receipt: SessionReceipt): void
980
+ /** Records a payment receipt. */
981
+ settleReceipt(receipt: SessionReceipt): void
982
+ /** Performs an automatic channel top-up when deposit is insufficient. */
983
+ topUpIfNeeded(parameters: TopUpRequirement): Promise<void>
984
+ /** Waits for the opening receipt before returning the managed socket. */
985
+ waitForReceipt(): Promise<SessionReceipt>
986
+ }
987
+
988
+ /** Inputs for validating a receipt belongs to the active WebSocket payment flow. */
989
+ export type ExpectedSocketReceiptParameters = {
990
+ /** Active WebSocket challenge ID. */
991
+ challengeId: string
992
+ /** Active WebSocket channel ID. */
993
+ channelId: Hex.Hex
994
+ /** Receipt received from a WebSocket payment frame. */
995
+ receipt: SessionReceipt
996
+ }
997
+
998
+ /** Inputs for validating a payment-close-ready frame. */
999
+ export type ValidateSocketCloseReadyReceiptParameters = ExpectedSocketReceiptParameters & {
1000
+ /** Local cumulative voucher authorization for the active channel. */
1001
+ cumulativeAmount: bigint
1002
+ }
1003
+
1004
+ /** Inputs for validating a payment-receipt frame. */
1005
+ export type ValidateSocketPaymentReceiptParameters = ExpectedSocketReceiptParameters & {
1006
+ /** Expected final close amount while a close credential is in flight. */
1007
+ expectedCloseAmount: string | null
1008
+ }
1009
+
1010
+ /** Probes a WebSocket endpoint over HTTP and creates the opening credential. */
1011
+ export async function prepareWebSocketSession(
1012
+ parameters: PrepareWebSocketSessionParameters,
1013
+ ): Promise<PreparedWebSocketSession> {
1014
+ const wsUrl = new URL(parameters.input.toString())
1015
+ const httpUrl = webSocketProbeUrl(wsUrl)
1016
+ parameters.onProbeUrl?.(httpUrl)
1017
+ const probe = await parameters.fetch(httpUrl, {
1018
+ ...parameters.probeInit,
1019
+ ...(parameters.signal ? { signal: parameters.signal } : {}),
1020
+ })
1021
+ if (probe.status !== 402) {
1022
+ throw new Error(
1023
+ `Expected a 402 payment challenge from ${httpUrl}, received ${probe.status} instead.`,
1024
+ )
1025
+ }
1026
+
1027
+ const challenge = Challenge.fromResponseList(probe).find(isTempoSessionChallenge)
1028
+ if (!challenge) {
1029
+ throw new Error(
1030
+ 'No payment challenge received from HTTP endpoint for this WebSocket URL. The server may not require payment or did not advertise a challenge.',
1031
+ )
1032
+ }
1033
+
1034
+ return {
1035
+ challenge,
1036
+ credential: await parameters.createSessionCredential(challenge, {}),
1037
+ httpUrl,
1038
+ wsUrl,
1039
+ }
1040
+ }
1041
+
1042
+ /** Creates the initial runtime state for a paid WebSocket from its opening credential. */
1043
+ export function createActiveSocketSession(
1044
+ parameters: CreateActiveSocketSessionParameters,
1045
+ ): ActiveSocketSession {
1046
+ const { challenge, credential, socket } = parameters
1047
+ return {
1048
+ challenge,
1049
+ channelId:
1050
+ PaymentCredential.deserialize<SessionCredentialPayload>(credential).payload.channelId,
1051
+ closeReadyReceipt: null,
1052
+ deliveredChunks: 0n,
1053
+ expectedCloseAmount: null,
1054
+ socket,
1055
+ tickCost: readSessionChallengeAmount(challenge),
1056
+ }
1057
+ }
1058
+
1059
+ /** Returns whether a receipt belongs to the active WebSocket session. */
1060
+ export function isExpectedSocketReceipt(parameters: ExpectedSocketReceiptParameters): boolean {
1061
+ const { challengeId, channelId, receipt } = parameters
1062
+ return receipt.challengeId === challengeId && receipt.channelId === channelId
1063
+ }
1064
+
1065
+ /** Returns a protocol failure message when a close-ready receipt is invalid. */
1066
+ export function validateSocketCloseReadyReceipt(
1067
+ parameters: ValidateSocketCloseReadyReceiptParameters,
1068
+ ): string | undefined {
1069
+ if (!isExpectedSocketReceipt(parameters)) return 'received mismatched payment-close-ready frame'
1070
+ if (BigInt(parameters.receipt.spent) > parameters.cumulativeAmount) {
1071
+ return 'received payment-close-ready beyond local voucher state'
1072
+ }
1073
+ return undefined
1074
+ }
1075
+
1076
+ /** Returns a protocol failure message when a payment receipt is invalid. */
1077
+ export function validateSocketPaymentReceipt(
1078
+ parameters: ValidateSocketPaymentReceiptParameters,
1079
+ ): string | undefined {
1080
+ const { challengeId, channelId, expectedCloseAmount, receipt } = parameters
1081
+ if (!isExpectedSocketReceipt(parameters)) return 'received mismatched payment-receipt frame'
1082
+ if (
1083
+ expectedCloseAmount !== null &&
1084
+ Boolean(receipt.txHash) &&
1085
+ !isExpectedCloseReceipt({ challengeId, channelId, expectedCloseAmount, receipt })
1086
+ ) {
1087
+ return 'received mismatched payment-close receipt frame'
1088
+ }
1089
+ return undefined
1090
+ }
1091
+
1092
+ /**
1093
+ * Opens an auto-driving paid WebSocket session.
1094
+ *
1095
+ * The driver owns socket protocol frames. Session state, credential creation,
1096
+ * and top-up policy remain supplied by `SessionManager`.
1097
+ */
1098
+ export async function openWebSocketSession(
1099
+ parameters: OpenWebSocketSessionParameters,
1100
+ ): Promise<SessionManagedWebSocket> {
1101
+ const { challenge, credential, options, WebSocket: WebSocketImpl, wsUrl } = parameters
1102
+ const rawSocket = new WebSocketImpl(wsUrl, options?.protocols)
1103
+ const socketState = createActiveSocketSession({
1104
+ challenge,
1105
+ credential,
1106
+ socket: rawSocket,
1107
+ })
1108
+ parameters.setSocketSession(socketState)
1109
+
1110
+ const managedSocket = createManagedSocket(rawSocket)
1111
+ const failSocketFlow = (message: string) => {
1112
+ parameters.rejectReceipt(new Error(message))
1113
+ parameters.rejectCloseReady(new Error(message))
1114
+ if (
1115
+ rawSocket.readyState === WebSocketReadyState.CONNECTING ||
1116
+ rawSocket.readyState === WebSocketReadyState.OPEN
1117
+ ) {
1118
+ rawSocket.close(ClientWebSocketProtocolErrorCloseCode, message)
1119
+ }
1120
+ }
1121
+ rawSocket.addEventListener('close', (event) => {
1122
+ socketState.socket = null
1123
+ socketState.expectedCloseAmount = null
1124
+ parameters.rejectReceipt(new Error('WebSocket closed before the payment flow completed.'))
1125
+ parameters.rejectCloseReady(new Error('WebSocket closed before the payment flow completed.'))
1126
+ managedSocket.emit('close', {
1127
+ code: (event as CloseEvent).code ?? 1000,
1128
+ reason: (event as CloseEvent).reason ?? '',
1129
+ type: 'close',
1130
+ wasClean: true,
1131
+ })
1132
+ })
1133
+ rawSocket.addEventListener('error', () => {
1134
+ managedSocket.emit('error', { type: 'error' })
1135
+ })
1136
+ rawSocket.addEventListener('message', async (event) => {
1137
+ const raw = typeof event.data === 'string' ? event.data : undefined
1138
+ if (!raw) return
1139
+ await handleSocketMessage({
1140
+ driver: parameters,
1141
+ failSocketFlow,
1142
+ managedEmit: managedSocket.emit,
1143
+ message: raw,
1144
+ rawSocket,
1145
+ socketState,
1146
+ })
1147
+ })
1148
+ options?.signal?.addEventListener(
1149
+ 'abort',
1150
+ () => {
1151
+ parameters.rejectReceipt(new Error('WebSocket payment flow aborted.'))
1152
+ parameters.rejectCloseReady(new Error('WebSocket payment flow aborted.'))
1153
+ rawSocket.close()
1154
+ },
1155
+ { once: true },
1156
+ )
1157
+
1158
+ await waitForSocketOpen(rawSocket, managedSocket.emit, wsUrl)
1159
+ rawSocket.send(Ws.formatAuthorizationMessage(credential))
1160
+ await parameters.waitForReceipt()
1161
+ return managedSocket.socket
1162
+ }
1163
+
1164
+ type ManagedEmit = ReturnType<typeof createManagedSocket>['emit']
1165
+
1166
+ function waitForSocketOpen(rawSocket: WebSocket, managedEmit: ManagedEmit, wsUrl: URL) {
1167
+ return new Promise<void>((resolve, reject) => {
1168
+ const onOpen = () => {
1169
+ rawSocket.removeEventListener('error', onError)
1170
+ managedEmit('open', { type: 'open' })
1171
+ resolve()
1172
+ }
1173
+ const onError = () => {
1174
+ rawSocket.removeEventListener('open', onOpen)
1175
+ reject(new Error(`WebSocket connection to ${wsUrl} failed to open.`))
1176
+ }
1177
+ rawSocket.addEventListener('open', onOpen, { once: true })
1178
+ rawSocket.addEventListener('error', onError, { once: true })
1179
+ })
1180
+ }
1181
+
1182
+ type HandleSocketMessageParameters = {
1183
+ driver: OpenWebSocketSessionParameters
1184
+ failSocketFlow(message: string): void
1185
+ managedEmit: ManagedEmit
1186
+ message: string
1187
+ rawSocket: WebSocket
1188
+ socketState: ActiveSocketSession
1189
+ }
1190
+
1191
+ async function handleSocketMessage(parameters: HandleSocketMessageParameters) {
1192
+ const parsed = Ws.parseMessage(parameters.message)
1193
+ if (!parsed) {
1194
+ parameters.managedEmit('message', { data: parameters.message, type: 'message' })
1195
+ return
1196
+ }
1197
+
1198
+ switch (parsed.mpp) {
1199
+ case 'authorization':
1200
+ return
1201
+ case 'message':
1202
+ parameters.socketState.deliveredChunks += 1n
1203
+ parameters.managedEmit('message', { data: parsed.data, type: 'message' })
1204
+ return
1205
+ case 'payment-close-ready':
1206
+ handleCloseReady(parameters, parsed.data)
1207
+ return
1208
+ case 'payment-error':
1209
+ parameters.driver.rejectReceipt(new Error(parsed.message))
1210
+ parameters.driver.rejectCloseReady(new Error(parsed.message))
1211
+ return
1212
+ case 'payment-need-voucher':
1213
+ await handleNeedVoucher(parameters, parsed.data)
1214
+ return
1215
+ case 'payment-receipt':
1216
+ handleReceipt(parameters, parsed.data)
1217
+ return
1218
+ }
1219
+ }
1220
+
1221
+ function handleCloseReady(parameters: HandleSocketMessageParameters, receipt: SessionReceipt) {
1222
+ const cumulativeAmount = parameters.driver.getChannel()?.cumulativeAmount ?? 0n
1223
+ const error = validateSocketCloseReadyReceipt({
1224
+ challengeId: parameters.driver.challenge.id,
1225
+ channelId: parameters.socketState.channelId,
1226
+ cumulativeAmount,
1227
+ receipt,
1228
+ })
1229
+ if (error) {
1230
+ parameters.failSocketFlow(error)
1231
+ return
1232
+ }
1233
+ parameters.driver.acceptReceipt(receipt)
1234
+ parameters.driver.options?.onReceipt?.(receipt)
1235
+ parameters.driver.settleCloseReady(receipt)
1236
+ parameters.managedEmit('close', {
1237
+ code: 1000,
1238
+ reason: 'stream complete',
1239
+ type: 'close',
1240
+ wasClean: true,
1241
+ })
1242
+ }
1243
+
1244
+ async function handleNeedVoucher(
1245
+ parameters: HandleSocketMessageParameters,
1246
+ event: NeedVoucherEvent,
1247
+ ) {
1248
+ try {
1249
+ const resolution = await resolveNeedVoucherContext({
1250
+ assertVoucherWithinLocalLimit: parameters.driver.assertVoucherWithinLocalLimit,
1251
+ challenge: parameters.driver.challenge,
1252
+ event,
1253
+ expectedChannelId: parameters.socketState.channelId,
1254
+ getChannel: parameters.driver.getChannel,
1255
+ input: parameters.driver.httpUrl,
1256
+ topUpIfNeeded: parameters.driver.topUpIfNeeded,
1257
+ })
1258
+ if (resolution.status === 'ignored') {
1259
+ parameters.failSocketFlow(
1260
+ resolution.reason === 'channel-mismatch'
1261
+ ? 'received mismatched payment-need-voucher frame'
1262
+ : 'cannot create voucher: no active channel',
1263
+ )
1264
+ return
1265
+ }
1266
+ const voucher = await parameters.driver.createSessionCredential(
1267
+ parameters.driver.challenge,
1268
+ resolution.context,
1269
+ )
1270
+ parameters.rawSocket.send(Ws.formatAuthorizationMessage(voucher))
1271
+ } catch (error) {
1272
+ parameters.failSocketFlow(
1273
+ error instanceof Error ? error.message : 'failed to create websocket voucher',
1274
+ )
1275
+ }
1276
+ }
1277
+
1278
+ function handleReceipt(parameters: HandleSocketMessageParameters, receipt: SessionReceipt) {
1279
+ const error = validateSocketPaymentReceipt({
1280
+ challengeId: parameters.driver.challenge.id,
1281
+ channelId: parameters.socketState.channelId,
1282
+ expectedCloseAmount: parameters.socketState.expectedCloseAmount,
1283
+ receipt,
1284
+ })
1285
+ if (error) {
1286
+ parameters.failSocketFlow(error)
1287
+ return
1288
+ }
1289
+ parameters.driver.acceptReceipt(receipt)
1290
+ parameters.driver.options?.onReceipt?.(receipt)
1291
+ parameters.driver.settleReceipt(receipt)
1292
+ }