mppx 0.0.1 → 0.1.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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/BodyDigest.d.ts +42 -0
  4. package/dist/BodyDigest.d.ts.map +1 -0
  5. package/dist/BodyDigest.js +40 -0
  6. package/dist/BodyDigest.js.map +1 -0
  7. package/dist/Challenge.d.ts +271 -0
  8. package/dist/Challenge.d.ts.map +1 -0
  9. package/dist/Challenge.js +291 -0
  10. package/dist/Challenge.js.map +1 -0
  11. package/dist/Credential.d.ts +91 -0
  12. package/dist/Credential.d.ts.map +1 -0
  13. package/dist/Credential.js +122 -0
  14. package/dist/Credential.js.map +1 -0
  15. package/dist/Errors.d.ts +243 -0
  16. package/dist/Errors.d.ts.map +1 -0
  17. package/dist/Errors.js +201 -0
  18. package/dist/Errors.js.map +1 -0
  19. package/dist/Expires.d.ts +15 -0
  20. package/dist/Expires.d.ts.map +1 -0
  21. package/dist/Expires.js +29 -0
  22. package/dist/Expires.js.map +1 -0
  23. package/dist/Intent.d.ts +101 -0
  24. package/dist/Intent.d.ts.map +1 -0
  25. package/dist/Intent.js +83 -0
  26. package/dist/Intent.js.map +1 -0
  27. package/dist/Mcp.d.ts +74 -0
  28. package/dist/Mcp.d.ts.map +1 -0
  29. package/dist/Mcp.js +9 -0
  30. package/dist/Mcp.js.map +1 -0
  31. package/dist/MethodIntent.d.ts +225 -0
  32. package/dist/MethodIntent.d.ts.map +1 -0
  33. package/dist/MethodIntent.js +156 -0
  34. package/dist/MethodIntent.js.map +1 -0
  35. package/dist/PaymentRequest.d.ts +88 -0
  36. package/dist/PaymentRequest.d.ts.map +1 -0
  37. package/dist/PaymentRequest.js +81 -0
  38. package/dist/PaymentRequest.js.map +1 -0
  39. package/dist/Receipt.d.ts +110 -0
  40. package/dist/Receipt.d.ts.map +1 -0
  41. package/dist/Receipt.js +105 -0
  42. package/dist/Receipt.js.map +1 -0
  43. package/dist/Store.d.ts +28 -0
  44. package/dist/Store.d.ts.map +1 -0
  45. package/dist/Store.js +61 -0
  46. package/dist/Store.js.map +1 -0
  47. package/dist/cli.d.ts +3 -0
  48. package/dist/cli.d.ts.map +1 -0
  49. package/dist/cli.js +1219 -0
  50. package/dist/cli.js.map +1 -0
  51. package/dist/client/Methods.d.ts +4 -0
  52. package/dist/client/Methods.d.ts.map +1 -0
  53. package/dist/client/Methods.js +4 -0
  54. package/dist/client/Methods.js.map +1 -0
  55. package/dist/client/Mppx.d.ts +84 -0
  56. package/dist/client/Mppx.d.ts.map +1 -0
  57. package/dist/client/Mppx.js +64 -0
  58. package/dist/client/Mppx.js.map +1 -0
  59. package/dist/client/Transport.d.ts +56 -0
  60. package/dist/client/Transport.d.ts.map +1 -0
  61. package/dist/client/Transport.js +81 -0
  62. package/dist/client/Transport.js.map +1 -0
  63. package/dist/client/index.d.ts +5 -0
  64. package/dist/client/index.d.ts.map +1 -0
  65. package/dist/client/index.js +5 -0
  66. package/dist/client/index.js.map +1 -0
  67. package/dist/client/internal/Fetch.d.ts +85 -0
  68. package/dist/client/internal/Fetch.d.ts.map +1 -0
  69. package/dist/client/internal/Fetch.js +95 -0
  70. package/dist/client/internal/Fetch.js.map +1 -0
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +13 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/internal/types.d.ts +302 -0
  76. package/dist/internal/types.d.ts.map +1 -0
  77. package/dist/internal/types.js +2 -0
  78. package/dist/internal/types.js.map +1 -0
  79. package/dist/mcp-sdk/client/McpClient.d.ts +78 -0
  80. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -0
  81. package/dist/mcp-sdk/client/McpClient.js +98 -0
  82. package/dist/mcp-sdk/client/McpClient.js.map +1 -0
  83. package/dist/mcp-sdk/client/index.d.ts +3 -0
  84. package/dist/mcp-sdk/client/index.d.ts.map +1 -0
  85. package/dist/mcp-sdk/client/index.js +3 -0
  86. package/dist/mcp-sdk/client/index.js.map +1 -0
  87. package/dist/mcp-sdk/server/Transport.d.ts +43 -0
  88. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -0
  89. package/dist/mcp-sdk/server/Transport.js +71 -0
  90. package/dist/mcp-sdk/server/Transport.js.map +1 -0
  91. package/dist/mcp-sdk/server/index.d.ts +2 -0
  92. package/dist/mcp-sdk/server/index.d.ts.map +1 -0
  93. package/dist/mcp-sdk/server/index.js +2 -0
  94. package/dist/mcp-sdk/server/index.js.map +1 -0
  95. package/dist/middlewares/elysia.d.ts +51 -0
  96. package/dist/middlewares/elysia.d.ts.map +1 -0
  97. package/dist/middlewares/elysia.js +59 -0
  98. package/dist/middlewares/elysia.js.map +1 -0
  99. package/dist/middlewares/express.d.ts +46 -0
  100. package/dist/middlewares/express.d.ts.map +1 -0
  101. package/dist/middlewares/express.js +69 -0
  102. package/dist/middlewares/express.js.map +1 -0
  103. package/dist/middlewares/hono.d.ts +46 -0
  104. package/dist/middlewares/hono.d.ts.map +1 -0
  105. package/dist/middlewares/hono.js +57 -0
  106. package/dist/middlewares/hono.js.map +1 -0
  107. package/dist/middlewares/internal/mppx.d.ts +16 -0
  108. package/dist/middlewares/internal/mppx.d.ts.map +1 -0
  109. package/dist/middlewares/internal/mppx.js +16 -0
  110. package/dist/middlewares/internal/mppx.js.map +1 -0
  111. package/dist/middlewares/nextjs.d.ts +45 -0
  112. package/dist/middlewares/nextjs.d.ts.map +1 -0
  113. package/dist/middlewares/nextjs.js +57 -0
  114. package/dist/middlewares/nextjs.js.map +1 -0
  115. package/dist/proxy/Proxy.d.ts +47 -0
  116. package/dist/proxy/Proxy.d.ts.map +1 -0
  117. package/dist/proxy/Proxy.js +126 -0
  118. package/dist/proxy/Proxy.js.map +1 -0
  119. package/dist/proxy/Service.d.ts +100 -0
  120. package/dist/proxy/Service.d.ts.map +1 -0
  121. package/dist/proxy/Service.js +147 -0
  122. package/dist/proxy/Service.js.map +1 -0
  123. package/dist/proxy/index.d.ts +7 -0
  124. package/dist/proxy/index.d.ts.map +1 -0
  125. package/dist/proxy/index.js +7 -0
  126. package/dist/proxy/index.js.map +1 -0
  127. package/dist/proxy/internal/Headers.d.ts +3 -0
  128. package/dist/proxy/internal/Headers.d.ts.map +1 -0
  129. package/dist/proxy/internal/Headers.js +41 -0
  130. package/dist/proxy/internal/Headers.js.map +1 -0
  131. package/dist/proxy/internal/Route.d.ts +14 -0
  132. package/dist/proxy/internal/Route.d.ts.map +1 -0
  133. package/dist/proxy/internal/Route.js +47 -0
  134. package/dist/proxy/internal/Route.js.map +1 -0
  135. package/dist/proxy/services/anthropic.d.ts +29 -0
  136. package/dist/proxy/services/anthropic.d.ts.map +1 -0
  137. package/dist/proxy/services/anthropic.js +30 -0
  138. package/dist/proxy/services/anthropic.js.map +1 -0
  139. package/dist/proxy/services/openai.d.ts +29 -0
  140. package/dist/proxy/services/openai.d.ts.map +1 -0
  141. package/dist/proxy/services/openai.js +30 -0
  142. package/dist/proxy/services/openai.js.map +1 -0
  143. package/dist/proxy/services/stripe.d.ts +29 -0
  144. package/dist/proxy/services/stripe.d.ts.map +1 -0
  145. package/dist/proxy/services/stripe.js +30 -0
  146. package/dist/proxy/services/stripe.js.map +1 -0
  147. package/dist/server/Methods.d.ts +3 -0
  148. package/dist/server/Methods.d.ts.map +1 -0
  149. package/dist/server/Methods.js +3 -0
  150. package/dist/server/Methods.js.map +1 -0
  151. package/dist/server/Mppx.d.ts +116 -0
  152. package/dist/server/Mppx.d.ts.map +1 -0
  153. package/dist/server/Mppx.js +207 -0
  154. package/dist/server/Mppx.js.map +1 -0
  155. package/dist/server/NodeListener.d.ts +3 -0
  156. package/dist/server/NodeListener.d.ts.map +1 -0
  157. package/dist/server/NodeListener.js +3 -0
  158. package/dist/server/NodeListener.js.map +1 -0
  159. package/dist/server/Request.d.ts +24 -0
  160. package/dist/server/Request.d.ts.map +1 -0
  161. package/dist/server/Request.js +26 -0
  162. package/dist/server/Request.js.map +1 -0
  163. package/dist/server/Response.d.ts +10 -0
  164. package/dist/server/Response.d.ts.map +1 -0
  165. package/dist/server/Response.js +15 -0
  166. package/dist/server/Response.js.map +1 -0
  167. package/dist/server/Transport.d.ts +93 -0
  168. package/dist/server/Transport.d.ts.map +1 -0
  169. package/dist/server/Transport.js +132 -0
  170. package/dist/server/Transport.js.map +1 -0
  171. package/dist/server/index.d.ts +9 -0
  172. package/dist/server/index.d.ts.map +1 -0
  173. package/dist/server/index.js +9 -0
  174. package/dist/server/index.js.map +1 -0
  175. package/dist/stripe/Intents.d.ts +54 -0
  176. package/dist/stripe/Intents.d.ts.map +1 -0
  177. package/dist/stripe/Intents.js +27 -0
  178. package/dist/stripe/Intents.js.map +1 -0
  179. package/dist/stripe/client/Charge.d.ts +114 -0
  180. package/dist/stripe/client/Charge.d.ts.map +1 -0
  181. package/dist/stripe/client/Charge.js +77 -0
  182. package/dist/stripe/client/Charge.js.map +1 -0
  183. package/dist/stripe/client/MethodIntents.d.ts +80 -0
  184. package/dist/stripe/client/MethodIntents.d.ts.map +1 -0
  185. package/dist/stripe/client/MethodIntents.js +34 -0
  186. package/dist/stripe/client/MethodIntents.js.map +1 -0
  187. package/dist/stripe/client/index.d.ts +3 -0
  188. package/dist/stripe/client/index.d.ts.map +1 -0
  189. package/dist/stripe/client/index.js +3 -0
  190. package/dist/stripe/client/index.js.map +1 -0
  191. package/dist/stripe/index.d.ts +2 -0
  192. package/dist/stripe/index.d.ts.map +1 -0
  193. package/dist/stripe/index.js +2 -0
  194. package/dist/stripe/index.js.map +1 -0
  195. package/dist/stripe/server/Charge.d.ts +74 -0
  196. package/dist/stripe/server/Charge.d.ts.map +1 -0
  197. package/dist/stripe/server/Charge.js +79 -0
  198. package/dist/stripe/server/Charge.js.map +1 -0
  199. package/dist/stripe/server/MethodIntents.d.ts +65 -0
  200. package/dist/stripe/server/MethodIntents.d.ts.map +1 -0
  201. package/dist/stripe/server/MethodIntents.js +21 -0
  202. package/dist/stripe/server/MethodIntents.js.map +1 -0
  203. package/dist/stripe/server/index.d.ts +3 -0
  204. package/dist/stripe/server/index.d.ts.map +1 -0
  205. package/dist/stripe/server/index.js +3 -0
  206. package/dist/stripe/server/index.js.map +1 -0
  207. package/dist/tempo/Attribution.d.ts +101 -0
  208. package/dist/tempo/Attribution.d.ts.map +1 -0
  209. package/dist/tempo/Attribution.js +124 -0
  210. package/dist/tempo/Attribution.js.map +1 -0
  211. package/dist/tempo/Intents.d.ts +132 -0
  212. package/dist/tempo/Intents.d.ts.map +1 -0
  213. package/dist/tempo/Intents.js +81 -0
  214. package/dist/tempo/Intents.js.map +1 -0
  215. package/dist/tempo/client/ChannelOps.d.ts +54 -0
  216. package/dist/tempo/client/ChannelOps.d.ts.map +1 -0
  217. package/dist/tempo/client/ChannelOps.js +138 -0
  218. package/dist/tempo/client/ChannelOps.js.map +1 -0
  219. package/dist/tempo/client/Charge.d.ts +76 -0
  220. package/dist/tempo/client/Charge.d.ts.map +1 -0
  221. package/dist/tempo/client/Charge.js +69 -0
  222. package/dist/tempo/client/Charge.js.map +1 -0
  223. package/dist/tempo/client/MethodIntents.d.ts +157 -0
  224. package/dist/tempo/client/MethodIntents.d.ts.map +1 -0
  225. package/dist/tempo/client/MethodIntents.js +25 -0
  226. package/dist/tempo/client/MethodIntents.js.map +1 -0
  227. package/dist/tempo/client/Session.d.ts +159 -0
  228. package/dist/tempo/client/Session.d.ts.map +1 -0
  229. package/dist/tempo/client/Session.js +263 -0
  230. package/dist/tempo/client/Session.js.map +1 -0
  231. package/dist/tempo/client/SessionManager.d.ts +62 -0
  232. package/dist/tempo/client/SessionManager.d.ts.map +1 -0
  233. package/dist/tempo/client/SessionManager.js +196 -0
  234. package/dist/tempo/client/SessionManager.js.map +1 -0
  235. package/dist/tempo/client/index.d.ts +6 -0
  236. package/dist/tempo/client/index.d.ts.map +1 -0
  237. package/dist/tempo/client/index.js +5 -0
  238. package/dist/tempo/client/index.js.map +1 -0
  239. package/dist/tempo/index.d.ts +3 -0
  240. package/dist/tempo/index.d.ts.map +1 -0
  241. package/dist/tempo/index.js +3 -0
  242. package/dist/tempo/index.js.map +1 -0
  243. package/dist/tempo/internal/account.d.ts +32 -0
  244. package/dist/tempo/internal/account.d.ts.map +1 -0
  245. package/dist/tempo/internal/account.js +33 -0
  246. package/dist/tempo/internal/account.js.map +1 -0
  247. package/dist/tempo/internal/defaults.d.ts +18 -0
  248. package/dist/tempo/internal/defaults.d.ts.map +1 -0
  249. package/dist/tempo/internal/defaults.js +18 -0
  250. package/dist/tempo/internal/defaults.js.map +1 -0
  251. package/dist/tempo/internal/types.d.ts +11 -0
  252. package/dist/tempo/internal/types.d.ts.map +1 -0
  253. package/dist/tempo/internal/types.js +2 -0
  254. package/dist/tempo/internal/types.js.map +1 -0
  255. package/dist/tempo/server/Charge.d.ts +77 -0
  256. package/dist/tempo/server/Charge.d.ts.map +1 -0
  257. package/dist/tempo/server/Charge.js +228 -0
  258. package/dist/tempo/server/Charge.js.map +1 -0
  259. package/dist/tempo/server/MethodIntents.d.ts +140 -0
  260. package/dist/tempo/server/MethodIntents.d.ts.map +1 -0
  261. package/dist/tempo/server/MethodIntents.js +26 -0
  262. package/dist/tempo/server/MethodIntents.js.map +1 -0
  263. package/dist/tempo/server/Session.d.ts +148 -0
  264. package/dist/tempo/server/Session.d.ts.map +1 -0
  265. package/dist/tempo/server/Session.js +529 -0
  266. package/dist/tempo/server/Session.js.map +1 -0
  267. package/dist/tempo/server/internal/transport.d.ts +47 -0
  268. package/dist/tempo/server/internal/transport.d.ts.map +1 -0
  269. package/dist/tempo/server/internal/transport.js +118 -0
  270. package/dist/tempo/server/internal/transport.js.map +1 -0
  271. package/dist/tempo/stream/Chain.d.ts +52 -0
  272. package/dist/tempo/stream/Chain.d.ts.map +1 -0
  273. package/dist/tempo/stream/Chain.js +215 -0
  274. package/dist/tempo/stream/Chain.js.map +1 -0
  275. package/dist/tempo/stream/Channel.d.ts +26 -0
  276. package/dist/tempo/stream/Channel.d.ts.map +1 -0
  277. package/dist/tempo/stream/Channel.js +27 -0
  278. package/dist/tempo/stream/Channel.js.map +1 -0
  279. package/dist/tempo/stream/ChannelStore.d.ts +103 -0
  280. package/dist/tempo/stream/ChannelStore.d.ts.map +1 -0
  281. package/dist/tempo/stream/ChannelStore.js +100 -0
  282. package/dist/tempo/stream/ChannelStore.js.map +1 -0
  283. package/dist/tempo/stream/Receipt.d.ts +22 -0
  284. package/dist/tempo/stream/Receipt.d.ts.map +1 -0
  285. package/dist/tempo/stream/Receipt.js +34 -0
  286. package/dist/tempo/stream/Receipt.js.map +1 -0
  287. package/dist/tempo/stream/Sse.d.ts +134 -0
  288. package/dist/tempo/stream/Sse.d.ts.map +1 -0
  289. package/dist/tempo/stream/Sse.js +288 -0
  290. package/dist/tempo/stream/Sse.js.map +1 -0
  291. package/dist/tempo/stream/Types.d.ts +78 -0
  292. package/dist/tempo/stream/Types.d.ts.map +1 -0
  293. package/dist/tempo/stream/Types.js +2 -0
  294. package/dist/tempo/stream/Types.js.map +1 -0
  295. package/dist/tempo/stream/Voucher.d.ts +20 -0
  296. package/dist/tempo/stream/Voucher.d.ts.map +1 -0
  297. package/dist/tempo/stream/Voucher.js +98 -0
  298. package/dist/tempo/stream/Voucher.js.map +1 -0
  299. package/dist/tempo/stream/escrow.abi.d.ts +598 -0
  300. package/dist/tempo/stream/escrow.abi.d.ts.map +1 -0
  301. package/dist/tempo/stream/escrow.abi.js +760 -0
  302. package/dist/tempo/stream/escrow.abi.js.map +1 -0
  303. package/dist/tempo/stream/index.d.ts +8 -0
  304. package/dist/tempo/stream/index.d.ts.map +1 -0
  305. package/dist/tempo/stream/index.js +8 -0
  306. package/dist/tempo/stream/index.js.map +1 -0
  307. package/dist/viem/Account.d.ts +12 -0
  308. package/dist/viem/Account.d.ts.map +1 -0
  309. package/dist/viem/Account.js +14 -0
  310. package/dist/viem/Account.js.map +1 -0
  311. package/dist/viem/Client.d.ts +21 -0
  312. package/dist/viem/Client.d.ts.map +1 -0
  313. package/dist/viem/Client.js +19 -0
  314. package/dist/viem/Client.js.map +1 -0
  315. package/dist/zod.d.ts +17 -0
  316. package/dist/zod.d.ts.map +1 -0
  317. package/dist/zod.js +35 -0
  318. package/dist/zod.js.map +1 -0
  319. package/package.json +117 -4
  320. package/src/BodyDigest.test.ts +43 -0
  321. package/src/BodyDigest.ts +53 -0
  322. package/src/Challenge.test-d.ts +81 -0
  323. package/src/Challenge.test.ts +414 -0
  324. package/src/Challenge.ts +429 -0
  325. package/src/Credential.test.ts +227 -0
  326. package/src/Credential.ts +154 -0
  327. package/src/Errors.test.ts +402 -0
  328. package/src/Errors.ts +348 -0
  329. package/src/Expires.ts +34 -0
  330. package/src/Intent.test.ts +180 -0
  331. package/src/Intent.ts +109 -0
  332. package/src/Mcp.ts +81 -0
  333. package/src/MethodIntent.test.ts +303 -0
  334. package/src/MethodIntent.ts +388 -0
  335. package/src/PaymentRequest.test.ts +152 -0
  336. package/src/PaymentRequest.ts +107 -0
  337. package/src/Receipt.test.ts +98 -0
  338. package/src/Receipt.ts +129 -0
  339. package/src/Store.ts +84 -0
  340. package/src/cli.test.ts +542 -0
  341. package/src/cli.ts +1319 -0
  342. package/src/client/Methods.ts +3 -0
  343. package/src/client/Mppx.test-d.ts +90 -0
  344. package/src/client/Mppx.test.ts +468 -0
  345. package/src/client/Mppx.ts +149 -0
  346. package/src/client/Transport.test.ts +283 -0
  347. package/src/client/Transport.ts +115 -0
  348. package/src/client/index.ts +4 -0
  349. package/src/client/internal/Fetch.test-d.ts +57 -0
  350. package/src/client/internal/Fetch.test.ts +281 -0
  351. package/src/client/internal/Fetch.ts +157 -0
  352. package/src/env.d.ts +11 -0
  353. package/src/index.ts +12 -0
  354. package/src/internal/types.ts +403 -0
  355. package/src/mcp-sdk/client/McpClient.test-d.ts +109 -0
  356. package/src/mcp-sdk/client/McpClient.test.ts +219 -0
  357. package/src/mcp-sdk/client/McpClient.ts +187 -0
  358. package/src/mcp-sdk/client/index.ts +2 -0
  359. package/src/mcp-sdk/server/Transport.ts +94 -0
  360. package/src/mcp-sdk/server/index.ts +1 -0
  361. package/src/middlewares/elysia.ts +66 -0
  362. package/src/middlewares/express.test.ts +155 -0
  363. package/src/middlewares/express.ts +82 -0
  364. package/src/middlewares/hono.test.ts +148 -0
  365. package/src/middlewares/hono.ts +62 -0
  366. package/src/middlewares/internal/mppx.ts +30 -0
  367. package/src/middlewares/nextjs.test.ts +164 -0
  368. package/src/middlewares/nextjs.ts +66 -0
  369. package/src/proxy/Proxy.test.ts +472 -0
  370. package/src/proxy/Proxy.ts +175 -0
  371. package/src/proxy/Service.test.ts +125 -0
  372. package/src/proxy/Service.ts +227 -0
  373. package/src/proxy/index.ts +6 -0
  374. package/src/proxy/internal/Headers.test.ts +100 -0
  375. package/src/proxy/internal/Headers.ts +40 -0
  376. package/src/proxy/internal/Route.test.ts +143 -0
  377. package/src/proxy/internal/Route.ts +54 -0
  378. package/src/proxy/services/anthropic.ts +45 -0
  379. package/src/proxy/services/openai.test.ts +97 -0
  380. package/src/proxy/services/openai.ts +48 -0
  381. package/src/proxy/services/stripe.ts +49 -0
  382. package/src/server/Methods.ts +2 -0
  383. package/src/server/Mppx.test-d.ts +343 -0
  384. package/src/server/Mppx.test.ts +342 -0
  385. package/src/server/Mppx.ts +378 -0
  386. package/src/server/NodeListener.test.ts +188 -0
  387. package/src/server/NodeListener.ts +3 -0
  388. package/src/server/Request.test.ts +102 -0
  389. package/src/server/Request.ts +33 -0
  390. package/src/server/Response.test.ts +31 -0
  391. package/src/server/Response.ts +27 -0
  392. package/src/server/Transport.test.ts +294 -0
  393. package/src/server/Transport.ts +222 -0
  394. package/src/server/index.ts +8 -0
  395. package/src/stripe/Charge.integration.test.ts +326 -0
  396. package/src/stripe/Intents.test.ts +52 -0
  397. package/src/stripe/Intents.ts +27 -0
  398. package/src/stripe/client/Charge.ts +119 -0
  399. package/src/stripe/client/MethodIntents.ts +37 -0
  400. package/src/stripe/client/index.ts +2 -0
  401. package/src/stripe/index.ts +1 -0
  402. package/src/stripe/server/Charge.ts +121 -0
  403. package/src/stripe/server/MethodIntents.ts +24 -0
  404. package/src/stripe/server/index.ts +2 -0
  405. package/src/tempo/Attribution.test.ts +187 -0
  406. package/src/tempo/Attribution.ts +156 -0
  407. package/src/tempo/Intents.test.ts +84 -0
  408. package/src/tempo/Intents.ts +93 -0
  409. package/src/tempo/client/ChannelOps.ts +233 -0
  410. package/src/tempo/client/Charge.ts +84 -0
  411. package/src/tempo/client/MethodIntents.ts +28 -0
  412. package/src/tempo/client/Session.ts +369 -0
  413. package/src/tempo/client/SessionManager.test.ts +223 -0
  414. package/src/tempo/client/SessionManager.ts +270 -0
  415. package/src/tempo/client/index.ts +5 -0
  416. package/src/tempo/index.ts +2 -0
  417. package/src/tempo/internal/account.ts +47 -0
  418. package/src/tempo/internal/defaults.ts +20 -0
  419. package/src/tempo/internal/types.ts +8 -0
  420. package/src/tempo/server/Charge.test.ts +847 -0
  421. package/src/tempo/server/Charge.ts +309 -0
  422. package/src/tempo/server/MethodIntents.ts +29 -0
  423. package/src/tempo/server/Session.test.ts +1349 -0
  424. package/src/tempo/server/Session.ts +773 -0
  425. package/src/tempo/server/Sse.test.ts +289 -0
  426. package/src/tempo/server/index.ts +5 -0
  427. package/src/tempo/server/internal/transport.ts +153 -0
  428. package/src/tempo/stream/Chain.ts +333 -0
  429. package/src/tempo/stream/Channel.ts +50 -0
  430. package/src/tempo/stream/ChannelStore.test.ts +473 -0
  431. package/src/tempo/stream/ChannelStore.ts +202 -0
  432. package/src/tempo/stream/Receipt.test.ts +84 -0
  433. package/src/tempo/stream/Receipt.ts +45 -0
  434. package/src/tempo/stream/Sse.test.ts +401 -0
  435. package/src/tempo/stream/Sse.ts +375 -0
  436. package/src/tempo/stream/Types.ts +86 -0
  437. package/src/tempo/stream/Voucher.test.ts +134 -0
  438. package/src/tempo/stream/Voucher.ts +123 -0
  439. package/src/tempo/stream/escrow.abi.ts +759 -0
  440. package/src/tempo/stream/index.ts +7 -0
  441. package/src/tsconfig.json +10 -0
  442. package/src/viem/Account.test.ts +71 -0
  443. package/src/viem/Account.ts +30 -0
  444. package/src/viem/Client.test.ts +58 -0
  445. package/src/viem/Client.ts +33 -0
  446. package/src/zod.ts +47 -0
package/src/cli.ts ADDED
@@ -0,0 +1,1319 @@
1
+ #!/usr/bin/env node
2
+ import * as child from 'node:child_process'
3
+ import * as fs from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import * as os from 'node:os'
6
+ import * as path from 'node:path'
7
+ import * as readline from 'node:readline'
8
+ import { cac } from 'cac'
9
+ import { Base64 } from 'ox'
10
+ import type { Chain } from 'viem'
11
+ import { type Address, createClient, http } from 'viem'
12
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
13
+ import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
14
+ import { type ZodMiniType, z } from 'zod/mini'
15
+ import * as Challenge from './Challenge.js'
16
+ import * as Credential from './Credential.js'
17
+ import * as Mppx from './client/Mppx.js'
18
+ import { tempo } from './tempo/client/index.js'
19
+ import type { StreamCredentialPayload } from './tempo/stream/Types.js'
20
+ import { signVoucher } from './tempo/stream/Voucher.js'
21
+
22
+ const require = createRequire(import.meta.url)
23
+ const { name, version } = require('../package.json') as { name: string; version: string }
24
+
25
+ const cli = cac(name)
26
+
27
+ cli
28
+ .command('[url]', 'Make HTTP request with automatic payment')
29
+ .option('-a, --account <name>', 'Account name (env: MPPX_ACCOUNT)')
30
+ .option('-d, --data <data>', 'Send request body (implies POST unless -X is set)')
31
+ .option('-f, --fail', 'Fail silently on HTTP errors (exit 22)')
32
+ .option('-i, --include', 'Include response headers in output')
33
+ .option('-k, --insecure', 'Skip TLS certificate verification (true for localhost/.local)')
34
+ .option(
35
+ '-r, --rpc-url <url>',
36
+ 'RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)',
37
+ )
38
+ .option('-s, --silent', 'Silent mode (suppress progress and info)')
39
+ .option('-v, --verbose', 'Show request/response headers')
40
+ .option('-A, --user-agent <ua>', 'Set User-Agent header')
41
+ .option('-H, --header <header>', 'Add header (repeatable)')
42
+ .option('-L, --location', 'Follow redirects')
43
+ .option('-X, --method <method>', 'HTTP method')
44
+ .option('--channel <id>', 'Reuse existing stream channel ID')
45
+ .option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
46
+ .option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
47
+ .option('--yes', 'Skip confirmation prompts')
48
+ .example(`${name} example.com/content`)
49
+ .example(`${name} example.com/api --json '{"key":"value"}'`)
50
+ .action(async (rawUrl: string | undefined, rawOptions: unknown) => {
51
+ const options = parseOptions(
52
+ z.object({
53
+ account: z.optional(z.string()),
54
+ channel: z.optional(z.coerce.string()),
55
+ data: z.optional(z.string()),
56
+ deposit: z.optional(z.union([z.string(), z.number()])),
57
+ fail: z.optional(z.boolean()),
58
+ header: z.optional(z.union([z.string(), z.array(z.string())])),
59
+ include: z.optional(z.boolean()),
60
+ insecure: z.optional(z.boolean()),
61
+ json: z.optional(z.string()),
62
+ location: z.optional(z.boolean()),
63
+ method: z.optional(z.string()),
64
+ rpcUrl: z.optional(z.string()),
65
+ silent: z.optional(z.boolean()),
66
+ userAgent: z.optional(z.string()),
67
+ verbose: z.optional(z.boolean()),
68
+ yes: z.optional(z.boolean()),
69
+ }),
70
+ rawOptions,
71
+ )
72
+ if (!rawUrl) {
73
+ cli.outputHelp()
74
+ return
75
+ }
76
+
77
+ const silent = options.silent ?? false
78
+ const info = silent ? (_msg: string) => {} : (msg: string) => process.stderr.write(msg)
79
+ if (silent) options.yes = true
80
+
81
+ const accountName = resolveAccountName(options.account)
82
+ const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get())
83
+ if (!privateKey) {
84
+ if (options.account) console.log(`Account "${accountName}" not found.`)
85
+ else console.log(`No account found.`)
86
+ process.exit(1)
87
+ }
88
+
89
+ const headers: Record<string, string> = {}
90
+ if (options.header) {
91
+ const headerList = Array.isArray(options.header) ? options.header : [options.header]
92
+ for (const header of headerList) {
93
+ const index = header.indexOf(':')
94
+ if (index === -1) {
95
+ console.error(`Invalid header format: ${header}`)
96
+ process.exit(1)
97
+ }
98
+ headers[header.slice(0, index).trim()] = header.slice(index + 1).trim()
99
+ }
100
+ }
101
+ headers['User-Agent'] = options.userAgent ?? `${name}/${version}`
102
+
103
+ const url = (() => {
104
+ const hasProtocol = /^https?:\/\//.test(rawUrl)
105
+ const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(rawUrl)
106
+ return hasProtocol ? rawUrl : `${isLocal ? 'http' : 'https'}://${rawUrl}`
107
+ })()
108
+ const { hostname } = new URL(url)
109
+ if (options.insecure || hostname === 'localhost' || hostname.endsWith('.local')) {
110
+ process.removeAllListeners('warning')
111
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
112
+ }
113
+
114
+ try {
115
+ const fetchInit: RequestInit = { redirect: options.location ? 'follow' : 'manual' }
116
+ if (options.json) {
117
+ fetchInit.body = options.json
118
+ headers['Content-Type'] ??= 'application/json'
119
+ headers.Accept ??= 'application/json'
120
+ } else if (options.data) {
121
+ fetchInit.body = options.data
122
+ }
123
+ if (options.method) fetchInit.method = options.method.toUpperCase()
124
+ else if (fetchInit.body) fetchInit.method = 'POST'
125
+ if (Object.keys(headers).length > 0) fetchInit.headers = headers
126
+
127
+ const verbose = options.verbose ?? false
128
+
129
+ const printRequestHeaders = (reqUrl: string, init: RequestInit) => {
130
+ if (!verbose) return
131
+ const { pathname, host } = new URL(reqUrl)
132
+ const method = (init.method ?? 'GET').toUpperCase()
133
+ info(`> ${method} ${pathname} HTTP/1.1\n`)
134
+ info(`> Host: ${host}\n`)
135
+ for (const [k, v] of Object.entries((init.headers ?? {}) as Record<string, string>))
136
+ info(`> ${k}: ${v}\n`)
137
+ info('>\n')
138
+ }
139
+
140
+ const printResponseHeaders = (res: Response) => {
141
+ if (!options.include && !verbose) return
142
+ if (silent) return
143
+ const status = `HTTP/1.1 ${res.status} ${res.statusText}`
144
+ const out = verbose ? process.stderr : process.stdout
145
+ const prefix = verbose ? '< ' : ''
146
+ out.write(`${prefix}${status}\n`)
147
+ for (const [k, v] of res.headers) out.write(`${prefix}${k}: ${v}\n`)
148
+ out.write(verbose ? '<\n' : '\n')
149
+ }
150
+
151
+ printRequestHeaders(url, fetchInit)
152
+ const challengeResponse = await globalThis.fetch(url, fetchInit)
153
+ if (challengeResponse.status !== 402) {
154
+ if (options.fail && challengeResponse.status >= 400) process.exit(22)
155
+ printResponseHeaders(challengeResponse)
156
+ console.log((await challengeResponse.text()).replace(/\n+$/, ''))
157
+ return
158
+ }
159
+
160
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
161
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
162
+ const client = createClient({
163
+ chain: await resolveChain({ ...options, rpcUrl }),
164
+ transport: http(rpcUrl),
165
+ })
166
+
167
+ const challenge = Challenge.fromResponse(challengeResponse)
168
+ const explorerUrl = client.chain?.blockExplorers?.default?.url
169
+ const shownKeys = new Set<string>()
170
+ const challengeRequest = challenge.request as Record<string, unknown>
171
+ const currency = challengeRequest.currency as string | undefined
172
+ const tokenInfo = currency
173
+ ? await fetchTokenInfo(client, currency as Address, account.address).catch(() => undefined)
174
+ : undefined
175
+ const tokenSymbol = tokenInfo?.symbol ?? currency ?? ''
176
+ const tokenDecimals =
177
+ tokenInfo?.decimals ?? (challengeRequest.decimals as number | undefined) ?? 6
178
+
179
+ {
180
+ printResponseHeaders(challengeResponse)
181
+ const request = challengeRequest
182
+
183
+ const balanceKeys = new Set(['amount', 'suggestedDeposit', 'minVoucherDelta'])
184
+ const skipKeys = new Set(['decimals', 'currency', 'methodDetails'])
185
+ const fmtRequestValue = (key: string, value: unknown): string => {
186
+ if (balanceKeys.has(key) && typeof value === 'string') {
187
+ return `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`
188
+ }
189
+ if (key === 'chainId' && typeof value === 'number') {
190
+ const name = chainName({ id: value, name: '' })
191
+ return name ? `${value} ${pc.dim(`(${name})`)}` : String(value)
192
+ }
193
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value))
194
+ return explorerUrl ? pc.link(`${explorerUrl}/address/${value}`, value) : value
195
+ if (typeof value === 'string' && /^https?:\/\//.test(value)) return pc.link(value, value)
196
+ return String(value)
197
+ }
198
+ const decodeMemo = (hex: string): string | undefined => {
199
+ try {
200
+ const stripped = hex.replace(/^0x0*/, '')
201
+ if (!stripped) return undefined
202
+ const bytes = Uint8Array.from(
203
+ stripped.match(/.{1,2}/g)!.map((b) => Number.parseInt(b, 16)),
204
+ )
205
+ const decoded = new TextDecoder().decode(bytes)
206
+ return /^[\x20-\x7e]+$/.test(decoded) ? decoded : undefined
207
+ } catch {
208
+ return undefined
209
+ }
210
+ }
211
+
212
+ const skipChallengeKeys = new Set(['id', 'request'])
213
+ const fmtChallengeValue = (key: string, value: unknown): string => {
214
+ if (key === 'realm' && typeof value === 'string') {
215
+ try {
216
+ const realmUrl = new URL(value.includes('://') ? value : `https://${value}`)
217
+ return pc.link(realmUrl.href, value)
218
+ } catch {}
219
+ }
220
+ return String(value)
221
+ }
222
+ const challengeRows: [string, string][] = []
223
+ for (const [key, value] of Object.entries(challenge)) {
224
+ if (skipChallengeKeys.has(key) || value === undefined) continue
225
+ challengeRows.push([key, fmtChallengeValue(key, value)])
226
+ }
227
+ challengeRows.sort(([a], [b]) => a.localeCompare(b))
228
+
229
+ const requestRows: [string, string][] = []
230
+ for (const [key, value] of Object.entries(request)) {
231
+ if (skipKeys.has(key) || value === undefined) continue
232
+ requestRows.push([key, fmtRequestValue(key, value)])
233
+ }
234
+ requestRows.sort(([a], [b]) => a.localeCompare(b))
235
+
236
+ const detailRows: [string, string, string?][] = []
237
+ const methodDetails = request.methodDetails as Record<string, unknown> | undefined
238
+ if (methodDetails) {
239
+ for (const [key, value] of Object.entries(methodDetails)) {
240
+ if (value === undefined) continue
241
+ if (key === 'memo' && typeof value === 'string') {
242
+ const decoded = decodeMemo(value)
243
+ detailRows.push([key, decoded ? `${decoded}\n${pc.dim(value)}` : value])
244
+ } else {
245
+ detailRows.push([key, fmtRequestValue(key, value)])
246
+ }
247
+ }
248
+ detailRows.sort(([a], [b]) => a.localeCompare(b))
249
+ }
250
+
251
+ const sections: [string, [string, string][]][] = [
252
+ ['Challenge', challengeRows],
253
+ ['Request', requestRows],
254
+ ...(detailRows.length ? [['Details', detailRows] as [string, [string, string][]]] : []),
255
+ ]
256
+ for (const [, rows] of sections) for (const [key] of rows) shownKeys.add(key)
257
+ const pad = Math.max(...sections.flatMap(([, rows]) => rows.map(([k]) => k.length)))
258
+ const indent = ` ${''.padEnd(pad)} `
259
+
260
+ info(`${pc.bold(pc.yellow('Payment Required'))}\n`)
261
+ for (const [title, rows] of sections) {
262
+ info(`${pc.bold(title)}\n`)
263
+ for (const [label, value] of rows) {
264
+ const [first, ...rest] = value.split('\n')
265
+ info(` ${pc.dim(label.padEnd(pad))} ${first}\n`)
266
+ for (const line of rest) info(`${indent}${line}\n`)
267
+ }
268
+ }
269
+ info('\n')
270
+
271
+ if (!options.yes) {
272
+ const ok = await confirm(`Proceed with ${challenge.intent}?`)
273
+ if (!ok) {
274
+ info('Aborted.\n')
275
+ process.exit(0)
276
+ }
277
+ }
278
+ }
279
+
280
+ const mppx = Mppx.create({
281
+ methods: tempo({
282
+ account,
283
+ getClient: () => client,
284
+ deposit: (() => {
285
+ if (challenge.intent !== 'session') return undefined
286
+ const suggestedDeposit = (challenge.request as Record<string, unknown>)
287
+ .suggestedDeposit as string | undefined
288
+ const cliDeposit = options.deposit !== undefined ? String(options.deposit) : undefined
289
+ const resolved =
290
+ suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain!) ? '10' : undefined)
291
+ if (!resolved) {
292
+ console.error(
293
+ 'Stream payment requires a deposit. Use --deposit <amount> or connect to testnet.',
294
+ )
295
+ process.exit(1)
296
+ }
297
+ return resolved
298
+ })(),
299
+ }),
300
+ polyfill: false,
301
+ })
302
+
303
+ const credential = await mppx.createCredential(
304
+ challengeResponse,
305
+ (() => {
306
+ if (!options.channel) return undefined
307
+ const idx = process.argv.indexOf('--channel')
308
+ const channelId = idx !== -1 ? process.argv[idx + 1]! : String(options.channel)
309
+ const saved = readChannelCumulative(channelId)
310
+ return {
311
+ channelId,
312
+ ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
313
+ }
314
+ })(),
315
+ )
316
+
317
+ const streamMd = challenge.request.methodDetails as
318
+ | { escrowContract?: string; chainId?: number }
319
+ | undefined
320
+ let streamChannelId: `0x${string}` | undefined
321
+ let streamEscrowContract: Address | undefined
322
+ let streamChainId = 0
323
+ let streamCumulativeAmount = 0n
324
+
325
+ if (challenge.intent === 'session') {
326
+ const parsed = Credential.deserialize<StreamCredentialPayload>(credential)
327
+ streamChannelId = parsed.payload.channelId
328
+ streamChainId = streamMd?.chainId ?? client.chain?.id ?? 0
329
+ streamEscrowContract = streamMd?.escrowContract as Address | undefined
330
+ if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
331
+ streamCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
332
+
333
+ if (parsed.payload.action === 'open') {
334
+ const depositRaw =
335
+ (challengeRequest.suggestedDeposit as string | undefined) ?? options.deposit
336
+ const depositDisplay = depositRaw
337
+ ? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
338
+ : ''
339
+ info(`${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`)
340
+ }
341
+ }
342
+
343
+ const credentialFetchInit = {
344
+ ...fetchInit,
345
+ headers: { ...(fetchInit.headers as Record<string, string>), Authorization: credential },
346
+ }
347
+ printRequestHeaders(url, credentialFetchInit)
348
+ const credentialResponse = await globalThis.fetch(url, credentialFetchInit)
349
+
350
+ if (options.fail && credentialResponse.status >= 400) process.exit(22)
351
+
352
+ if (credentialResponse.status === 402) {
353
+ const body = await credentialResponse.text()
354
+ info(`${pc.bold(pc.red('Payment Rejected'))}\n`)
355
+ try {
356
+ const problem = JSON.parse(body) as Record<string, unknown>
357
+ const rows: [string, string][] = []
358
+ for (const [key, value] of Object.entries(problem)) {
359
+ if (value === undefined) continue
360
+ rows.push([key, String(value)])
361
+ }
362
+ rows.sort(([a], [b]) => a.localeCompare(b))
363
+ const pad = Math.max(...rows.map(([k]) => k.length))
364
+ for (const [label, value] of rows) info(` ${pc.dim(label.padEnd(pad))} ${value}\n`)
365
+ } catch {
366
+ if (body) info(` ${body}\n`)
367
+ }
368
+ process.exit(1)
369
+ } else {
370
+ printResponseHeaders(credentialResponse)
371
+
372
+ const receiptHeader = credentialResponse.headers.get('Payment-Receipt')
373
+ if (receiptHeader) {
374
+ try {
375
+ const receiptJson = JSON.parse(Base64.toString(receiptHeader)) as Record<
376
+ string,
377
+ unknown
378
+ >
379
+ if (
380
+ typeof receiptJson.acceptedCumulative === 'string' &&
381
+ receiptJson.acceptedCumulative
382
+ ) {
383
+ streamCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
384
+ if (streamChannelId) writeChannelCumulative(streamChannelId, streamCumulativeAmount)
385
+ }
386
+ info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`)
387
+ const rows: [string, string][] = []
388
+ for (const [key, value] of Object.entries(receiptJson)) {
389
+ if (value === undefined || shownKeys.has(key)) continue
390
+ if (key === 'reference' && typeof value === 'string' && explorerUrl)
391
+ rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
392
+ else rows.push([key, String(value)])
393
+ }
394
+ rows.sort(([a], [b]) => a.localeCompare(b))
395
+ const pad = Math.max(...rows.map(([k]) => k.length))
396
+ for (const [label, value] of rows) info(` ${pc.dim(label.padEnd(pad))} ${value}\n`)
397
+ info('\n')
398
+ } catch {}
399
+ }
400
+ const contentType = credentialResponse.headers.get('Content-Type') ?? ''
401
+ if (contentType.includes('text/event-stream')) {
402
+ const reader = credentialResponse.body?.getReader()
403
+ if (!reader) {
404
+ console.error('No response body')
405
+ process.exit(1)
406
+ }
407
+ const decoder = new TextDecoder()
408
+ let buffer = ''
409
+ let currentEvent = ''
410
+
411
+ const streamCred =
412
+ challenge.intent === 'session'
413
+ ? Credential.deserialize<StreamCredentialPayload>(credential)
414
+ : undefined
415
+ const channelId = streamCred?.payload.channelId
416
+ const md = challenge.request.methodDetails as
417
+ | { escrowContract?: string; chainId?: number }
418
+ | undefined
419
+ const streamChainId = md?.chainId ?? client.chain?.id ?? 0
420
+ const escrowContract = md?.escrowContract as Address | undefined
421
+ let cumulativeAmount =
422
+ streamCred?.payload &&
423
+ 'cumulativeAmount' in streamCred.payload &&
424
+ streamCred.payload.cumulativeAmount
425
+ ? BigInt(streamCred.payload.cumulativeAmount)
426
+ : 0n
427
+ let _voucherSeq = 0
428
+
429
+ const termBg = await detectTerminalBg()
430
+ const chunkBgs = (() => {
431
+ if (!termBg || !pc.isColorSupported) return undefined
432
+ const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)))
433
+ const isDark = 0.299 * termBg.r + 0.587 * termBg.g + 0.114 * termBg.b < 128
434
+ const offset = isDark ? 1 : -1
435
+ const bgRgb = (d: number) => (s: string) => {
436
+ const r = clamp(termBg.r + d * offset)
437
+ const g = clamp(termBg.g + d * offset)
438
+ const b = clamp(termBg.b + d * offset)
439
+ return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`
440
+ }
441
+ return [bgRgb(12), bgRgb(24)] as const
442
+ })()
443
+ let chunkIdx = 0
444
+
445
+ const writeContent = (chunk: string) => {
446
+ if (chunkBgs) {
447
+ const bgFn = chunkBgs[chunkIdx % chunkBgs.length]!
448
+ process.stdout.write(chunk.replace(/[^\n]+/g, (m) => bgFn(m)))
449
+ chunkIdx++
450
+ } else {
451
+ process.stdout.write(chunk)
452
+ }
453
+ }
454
+
455
+ const processLines = async (lines: string[]) => {
456
+ for (const line of lines) {
457
+ if (line.startsWith('event: ')) {
458
+ currentEvent = line.slice(7).trim()
459
+ continue
460
+ }
461
+ if (!line.startsWith('data: ')) {
462
+ if (line === '') currentEvent = ''
463
+ continue
464
+ }
465
+ const data = line.slice(6)
466
+ if (data.trim() === '[DONE]') continue
467
+ if (
468
+ currentEvent === 'payment-need-voucher' &&
469
+ channelId &&
470
+ escrowContract &&
471
+ streamChainId
472
+ ) {
473
+ try {
474
+ const event = JSON.parse(data) as {
475
+ channelId: string
476
+ requiredCumulative: string
477
+ }
478
+ const required = BigInt(event.requiredCumulative)
479
+ cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required
480
+
481
+ const signature = await signVoucher(
482
+ client,
483
+ account,
484
+ { channelId, cumulativeAmount },
485
+ escrowContract,
486
+ streamChainId,
487
+ )
488
+ const voucherCred = Credential.serialize({
489
+ challenge,
490
+ payload: {
491
+ action: 'voucher',
492
+ channelId,
493
+ cumulativeAmount: cumulativeAmount.toString(),
494
+ signature,
495
+ },
496
+ source: `did:pkh:eip155:${streamChainId}:${account.address}`,
497
+ })
498
+ await globalThis.fetch(url, {
499
+ method: 'POST',
500
+ headers: { Authorization: voucherCred },
501
+ })
502
+ _voucherSeq++
503
+ } catch (e) {
504
+ info(
505
+ pc.dim(pc.yellow(` [voucher failed: ${e instanceof Error ? e.message : e}]`)),
506
+ )
507
+ }
508
+ currentEvent = ''
509
+ continue
510
+ }
511
+ if (currentEvent === 'payment-receipt') {
512
+ try {
513
+ const receipt = JSON.parse(data) as Record<string, unknown>
514
+ info(`\n\n${pc.bold(pc.green('Payment Receipt'))}\n`)
515
+ const rows: [string, string][] = []
516
+ for (const [key, value] of Object.entries(receipt)) {
517
+ if (value === undefined || shownKeys.has(key)) continue
518
+ if (key === 'channelId' && value === receipt.reference) continue
519
+ const receiptBalanceKeys = ['acceptedCumulative', 'spent']
520
+ if (receiptBalanceKeys.includes(key) && typeof value === 'string') {
521
+ rows.push([
522
+ key,
523
+ `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
524
+ ])
525
+ } else if (key === 'reference' && typeof value === 'string' && explorerUrl)
526
+ rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
527
+ else rows.push([key, String(value)])
528
+ }
529
+ rows.sort(([a], [b]) => a.localeCompare(b))
530
+ const rpad = Math.max(...rows.map(([k]) => k.length))
531
+ for (const [label, value] of rows)
532
+ info(` ${pc.dim(label.padEnd(rpad))} ${value}\n`)
533
+ } catch {}
534
+ currentEvent = ''
535
+ continue
536
+ }
537
+ if (data.length === 0) {
538
+ writeContent('\n')
539
+ } else {
540
+ try {
541
+ const parsed = JSON.parse(data) as {
542
+ token?: string
543
+ choices?: { delta?: { content?: string } }[]
544
+ }
545
+
546
+ writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data)
547
+ } catch {
548
+ writeContent(data)
549
+ }
550
+ }
551
+ currentEvent = ''
552
+ }
553
+ }
554
+
555
+ while (true) {
556
+ const { done, value } = await reader.read()
557
+ if (done) break
558
+ buffer += decoder.decode(value, { stream: true })
559
+ const lines = buffer.split('\n')
560
+ buffer = lines.pop()!
561
+ await processLines(lines)
562
+ }
563
+ if (buffer.trim()) await processLines([buffer])
564
+
565
+ if (channelId && escrowContract && streamChainId) {
566
+ const signature = await signVoucher(
567
+ client,
568
+ account,
569
+ { channelId, cumulativeAmount },
570
+ escrowContract,
571
+ streamChainId,
572
+ )
573
+ const closePayload: StreamCredentialPayload = {
574
+ action: 'close',
575
+ channelId,
576
+ cumulativeAmount: cumulativeAmount.toString(),
577
+ signature,
578
+ }
579
+ const closeCred = Credential.serialize({
580
+ challenge,
581
+ payload: closePayload,
582
+ source: `did:pkh:eip155:${streamChainId}:${account.address}`,
583
+ })
584
+ const closeRes = await globalThis.fetch(url, {
585
+ method: 'POST',
586
+ headers: { Authorization: closeCred },
587
+ })
588
+ if (closeRes.ok) {
589
+ info(
590
+ `\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(cumulativeAmount, tokenSymbol, tokenDecimals)}.`)}\n`,
591
+ )
592
+ } else {
593
+ info(
594
+ `\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
595
+ )
596
+ }
597
+ }
598
+ } else {
599
+ const body = (await credentialResponse.text()).replace(/\n+$/, '')
600
+ console.log(body)
601
+
602
+ const shouldClose =
603
+ challenge.intent === 'session' &&
604
+ credentialResponse.ok &&
605
+ streamChannelId &&
606
+ streamEscrowContract &&
607
+ streamChainId
608
+ if (shouldClose && !options.yes) {
609
+ info('\n')
610
+ }
611
+ if (shouldClose && !(options.yes || (await confirm('Close channel?')))) {
612
+ info(`${pc.dim('Kept channel open.')}\n`)
613
+ } else if (shouldClose) {
614
+ const signature = await signVoucher(
615
+ client,
616
+ account,
617
+ { channelId: streamChannelId!, cumulativeAmount: streamCumulativeAmount },
618
+ streamEscrowContract!,
619
+ streamChainId,
620
+ )
621
+ const closePayload: StreamCredentialPayload = {
622
+ action: 'close',
623
+ channelId: streamChannelId!,
624
+ cumulativeAmount: streamCumulativeAmount.toString(),
625
+ signature,
626
+ }
627
+ const closeCred = Credential.serialize({
628
+ challenge,
629
+ payload: closePayload,
630
+ source: `did:pkh:eip155:${streamChainId}:${account.address}`,
631
+ })
632
+ const closeRes = await globalThis.fetch(url, {
633
+ ...fetchInit,
634
+ headers: {
635
+ ...(fetchInit.headers as Record<string, string>),
636
+ Authorization: closeCred,
637
+ },
638
+ })
639
+ if (closeRes.ok) {
640
+ deleteChannelState(streamChannelId!)
641
+ info(
642
+ `${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(streamCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}\n`,
643
+ )
644
+ } else {
645
+ const closeBody = await closeRes.text().catch(() => '')
646
+ info(
647
+ `${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
648
+ )
649
+ info(
650
+ `${pc.dim(` channelId: ${streamChannelId}`)}\n` +
651
+ `${pc.dim(` cumulativeAmount: ${streamCumulativeAmount}`)}\n` +
652
+ `${pc.dim(` escrowContract: ${streamEscrowContract}`)}\n` +
653
+ `${pc.dim(` chainId: ${streamChainId}`)}\n` +
654
+ `${pc.dim(` account: ${account.address}`)}\n` +
655
+ `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`,
656
+ )
657
+ }
658
+ }
659
+ }
660
+ }
661
+ } catch (err) {
662
+ // TODO: revert cast when https://github.com/wevm/zile/pull/26 is merged
663
+ const errCause =
664
+ err instanceof Error ? (err as unknown as Record<string, unknown>).cause : undefined
665
+ const cause = errCause instanceof Error ? errCause : undefined
666
+
667
+ if (cause && 'code' in cause) {
668
+ const code = cause.code as string
669
+ if (code === 'ENOTFOUND')
670
+ console.error(`Could not resolve host "${hostname}". Check the URL and try again.`)
671
+ else if (code === 'ECONNREFUSED')
672
+ console.error(`Connection refused by "${hostname}". Is the server running?`)
673
+ else if (code === 'ECONNRESET') console.error(`Connection to "${hostname}" was reset.`)
674
+ else if (code === 'ETIMEDOUT') console.error(`Connection to "${hostname}" timed out.`)
675
+ else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
676
+ console.error(
677
+ `TLS certificate error for "${hostname}". Use --insecure to skip verification.`,
678
+ )
679
+ else {
680
+ console.error(`Request to "${hostname}" failed: ${cause.message}`)
681
+ }
682
+ } else {
683
+ console.error('Request failed:', err instanceof Error ? err.message : err)
684
+ if (cause) console.error('Cause:', cause.message)
685
+ }
686
+ process.exit(1)
687
+ }
688
+ })
689
+
690
+ const accountOptionsSchema = z.object({
691
+ account: z.optional(z.string()),
692
+ rpcUrl: z.optional(z.string()),
693
+ yes: z.optional(z.boolean()),
694
+ })
695
+
696
+ cli
697
+ .command('account [action]', 'Manage accounts (create, default, delete, fund, list, view)')
698
+ .option('-a, --account <name>', 'Account name (env: MPPX_ACCOUNT)')
699
+ .option(
700
+ '-r, --rpc-url <url>',
701
+ 'RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)',
702
+ )
703
+ .option('--yes', 'DANGER!! Skip confirmation prompts')
704
+ .action(async (action: string | undefined, rawOptions: unknown) => {
705
+ if (!action) {
706
+ cli.outputHelp()
707
+ return
708
+ }
709
+ const options = parseOptions(accountOptionsSchema, rawOptions)
710
+ switch (action) {
711
+ case 'create': {
712
+ let resolvedName = options.account
713
+ if (!resolvedName) {
714
+ const existing = await createKeychain().list()
715
+ if (existing.length === 0) resolvedName = 'main'
716
+ else {
717
+ const input = await prompt('Account name')
718
+ if (!input) return
719
+ resolvedName = input
720
+ }
721
+ }
722
+ let keychain = createKeychain(resolvedName)
723
+ while (await keychain.get()) {
724
+ process.stderr.write(`${pc.dim(`Account "${resolvedName}" already exists.`)}\n\n`)
725
+ const input = await prompt('Enter different name')
726
+ if (!input) return
727
+ resolvedName = input
728
+ keychain = createKeychain(resolvedName)
729
+ }
730
+ const privateKey = generatePrivateKey()
731
+ const account = privateKeyToAccount(privateKey)
732
+ await keychain.set(privateKey)
733
+ const accounts = await createKeychain().list()
734
+ if (accounts.length === 1) createDefaultStore().set(resolvedName)
735
+ console.log(`Account "${resolvedName}" saved to keychain.`)
736
+ console.log(pc.dim(`Address ${account.address}`))
737
+ resolveChain(options)
738
+ .then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
739
+ .then((client) =>
740
+ import('viem/tempo').then(({ Actions }) =>
741
+ Actions.faucet.fund(client, { account }).catch(() => {}),
742
+ ),
743
+ )
744
+ return
745
+ }
746
+ case 'default': {
747
+ const accountName = options.account
748
+ if (!accountName) {
749
+ console.error('-a, --account <name> is required for default.')
750
+ process.exit(1)
751
+ }
752
+ const key = await createKeychain(accountName).get()
753
+ if (!key) {
754
+ console.log(`Account "${accountName}" not found.`)
755
+ process.exit(1)
756
+ }
757
+ createDefaultStore().set(accountName)
758
+ console.log(`Default account set to "${accountName}"`)
759
+ return
760
+ }
761
+ case 'delete': {
762
+ if (!options.account) {
763
+ console.error('-a, --account <name> is required for delete.')
764
+ process.exit(1)
765
+ }
766
+ const keychain = createKeychain(options.account)
767
+ const key = await keychain.get()
768
+ if (!key) {
769
+ console.log(`Account "${options.account}" not found.`)
770
+ process.exit(1)
771
+ }
772
+ const account = privateKeyToAccount(key as `0x${string}`)
773
+ const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false })
774
+ if (!options.yes) {
775
+ process.stderr.write(pc.dim(`Delete account "${options.account}"\n`))
776
+ process.stderr.write(pc.dim(` Address ${account.address}\n`))
777
+ for (let i = 0; i < balanceLines.length; i++)
778
+ process.stderr.write(
779
+ pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`),
780
+ )
781
+ process.stderr.write(pc.dim('This action cannot be undone\n\n'))
782
+ const confirmed = await confirm('Confirm delete?')
783
+ if (!confirmed) {
784
+ console.log('Canceled')
785
+ return
786
+ }
787
+ }
788
+ await keychain.delete()
789
+ const currentDefault = createDefaultStore().get()
790
+ if (currentDefault === options.account) {
791
+ const remaining = await createKeychain().list()
792
+ if (remaining.length > 0) {
793
+ createDefaultStore().set(remaining[0]!)
794
+ console.log(`Default account set to "${remaining[0]}"`)
795
+ } else {
796
+ createDefaultStore().clear()
797
+ }
798
+ }
799
+ console.log(`Account "${options.account}" deleted`)
800
+ return
801
+ }
802
+ case 'fund': {
803
+ const accountName = resolveAccountName(options.account)
804
+ const keychain = createKeychain(accountName)
805
+ const key = await keychain.get()
806
+ if (!key) {
807
+ if (options.account) console.log(`Account "${accountName}" not found.`)
808
+ else console.log(`No account found.`)
809
+ process.exit(1)
810
+ }
811
+ const account = privateKeyToAccount(key as `0x${string}`)
812
+ const chain = await resolveChain(options)
813
+ const client = createClient({ chain, transport: http(options.rpcUrl) })
814
+ console.log(`Funding "${accountName}" on ${chainName(chain)}`)
815
+ try {
816
+ const { Actions } = await import('viem/tempo')
817
+ const hashes = await Actions.faucet.fund(client, { account })
818
+ const explorerUrl = chain.blockExplorers?.default?.url
819
+ for (const hash of hashes) {
820
+ const label = explorerUrl ? pc.link(`${explorerUrl}/tx/${hash}`, pc.gray(hash)) : hash
821
+ console.log(` ${label}`)
822
+ }
823
+ const { waitForTransactionReceipt } = await import('viem/actions')
824
+ await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })))
825
+ console.log('Funded successfully')
826
+ } catch (err) {
827
+ console.error('Funding failed:', err instanceof Error ? err.message : err)
828
+ }
829
+ return
830
+ }
831
+ case 'list': {
832
+ const currentDefault = createDefaultStore().get()
833
+ const accounts = (await createKeychain().list()).sort()
834
+ if (accounts.length === 0) {
835
+ console.log(`No accounts found.`)
836
+ return
837
+ }
838
+ const entries = await Promise.all(
839
+ accounts.map(async (accountName) => {
840
+ const key = await createKeychain(accountName).get()
841
+ if (!key) return undefined
842
+ return {
843
+ name: accountName,
844
+ address: privateKeyToAccount(key as `0x${string}`).address,
845
+ }
846
+ }),
847
+ )
848
+ const resolved = entries.filter((e) => e !== undefined)
849
+ const maxWidth = Math.max(
850
+ ...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)),
851
+ )
852
+ for (const entry of resolved) {
853
+ const isDefault = entry.name === currentDefault
854
+ const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name
855
+ const width = entry.name.length + (isDefault ? 1 : 0)
856
+ console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(entry.address)}`)
857
+ }
858
+ return
859
+ }
860
+ case 'view': {
861
+ const accountName = resolveAccountName(options.account)
862
+ const keychain = createKeychain(accountName)
863
+ const key = await keychain.get()
864
+ if (!key) {
865
+ if (options.account) console.log(`Account "${accountName}" not found.`)
866
+ else console.log(`No account found.`)
867
+ process.exit(1)
868
+ }
869
+ const account = privateKeyToAccount(key as `0x${string}`)
870
+ console.log(`${pc.dim('Address')} ${account.address}`)
871
+
872
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
873
+ const chain = rpcUrl ? await resolveChain({ rpcUrl }) : undefined
874
+ const balanceLines = await fetchBalanceLines(
875
+ account.address,
876
+ chain && rpcUrl ? { chain, rpcUrl } : undefined,
877
+ )
878
+ for (let i = 0; i < balanceLines.length; i++)
879
+ console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`)
880
+
881
+ console.log(`${pc.dim('Name')} ${accountName}`)
882
+ return
883
+ }
884
+ default:
885
+ console.error(`Unknown action: ${action}`)
886
+ console.error('Available: create, default, delete, fund, list, view')
887
+ process.exit(1)
888
+ }
889
+ })
890
+
891
+ cli.version(version, '-V, --version')
892
+
893
+ cli.help((sections) => {
894
+ const isAccount = sections.some((s: { body?: string }) => s.body?.includes('$ mppx account'))
895
+ if (isAccount) {
896
+ const actionsSection = {
897
+ title: 'Actions',
898
+ body: [
899
+ ' create Create new account',
900
+ ' default Set default account',
901
+ ' delete Delete account',
902
+ ' fund Fund account with testnet tokens',
903
+ ' list List all accounts',
904
+ ' view View account address',
905
+ ].join('\n'),
906
+ }
907
+ const optionsIndex = sections.findIndex((s: { title?: string }) => s.title === 'Options')
908
+ if (optionsIndex !== -1) sections.splice(optionsIndex, 0, actionsSection)
909
+ else sections.push(actionsSection)
910
+ }
911
+ return sections
912
+ })
913
+
914
+ try {
915
+ cli.parse()
916
+ } catch (err) {
917
+ console.error(err instanceof Error ? err.message : err)
918
+ process.exit(1)
919
+ }
920
+
921
+ /////////////////////////////////////////////////////////////////////////////////////////////////
922
+
923
+ function parseOptions<const schema extends ZodMiniType>(
924
+ schema: schema,
925
+ rawOptions: unknown,
926
+ ): z.output<schema> {
927
+ const result = schema.safeParse(rawOptions ?? {})
928
+ if (result.success) return result.data
929
+ const summary = result.error.issues
930
+ .map((issue) => {
931
+ const path = issue.path.length ? issue.path.join('.') : 'options'
932
+ return `${path}: ${issue.message}`
933
+ })
934
+ .join(', ')
935
+ throw new Error(`Invalid CLI options (${summary})`)
936
+ }
937
+
938
+ function execCommand(
939
+ command: string,
940
+ args: string[],
941
+ ): Promise<{ stdout: string; stderr: string; error: Error | null }> {
942
+ return new Promise((resolve) => {
943
+ child.execFile(command, args, (error, stdout, stderr) => {
944
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), error })
945
+ })
946
+ })
947
+ }
948
+
949
+ function channelStateDir() {
950
+ return path.join(
951
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
952
+ 'mppx',
953
+ 'channels',
954
+ )
955
+ }
956
+
957
+ function readChannelCumulative(channelId: string): bigint | undefined {
958
+ try {
959
+ const raw = fs.readFileSync(path.join(channelStateDir(), channelId), 'utf-8').trim()
960
+ return raw ? BigInt(raw) : undefined
961
+ } catch {
962
+ return undefined
963
+ }
964
+ }
965
+
966
+ function writeChannelCumulative(channelId: string, cumulative: bigint): void {
967
+ const dir = channelStateDir()
968
+ fs.mkdirSync(dir, { recursive: true })
969
+ fs.writeFileSync(path.join(dir, channelId), cumulative.toString(), 'utf-8')
970
+ }
971
+
972
+ function deleteChannelState(channelId: string): void {
973
+ try {
974
+ fs.unlinkSync(path.join(channelStateDir(), channelId))
975
+ } catch {}
976
+ }
977
+
978
+ function createDefaultStore() {
979
+ const configPath = path.join(
980
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
981
+ 'mppx',
982
+ 'default',
983
+ )
984
+ return {
985
+ get(): string {
986
+ try {
987
+ return fs.readFileSync(configPath, 'utf-8').trim() || 'main'
988
+ } catch {
989
+ return 'main'
990
+ }
991
+ },
992
+ set(value: string): void {
993
+ fs.mkdirSync(path.dirname(configPath), { recursive: true })
994
+ fs.writeFileSync(configPath, value, 'utf-8')
995
+ },
996
+ clear(): void {
997
+ try {
998
+ fs.unlinkSync(configPath)
999
+ } catch {}
1000
+ },
1001
+ }
1002
+ }
1003
+
1004
+ function resolveAccountName(explicit?: string): string {
1005
+ if (explicit) return explicit
1006
+ if (process.env.MPPX_ACCOUNT) return process.env.MPPX_ACCOUNT
1007
+ return createDefaultStore().get()
1008
+ }
1009
+
1010
+ // biome-ignore format: compact shell commands
1011
+ function createKeychain(account = 'main') {
1012
+ const service = name
1013
+ return {
1014
+ async list(): Promise<string[]> {
1015
+ const platform = os.platform()
1016
+ if (platform === 'darwin') {
1017
+ const { stdout, error } = await execCommand('security', ['dump-keychain'])
1018
+ if (error) return []
1019
+ const accounts: string[] = []
1020
+ const blocks = stdout.split('keychain:')
1021
+ for (const block of blocks) {
1022
+ const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/)
1023
+ const accountMatch = block.match(/"acct"<blob>="([^"]*)"/)
1024
+ if (serviceMatch?.[1] === service && accountMatch?.[1]) accounts.push(accountMatch[1])
1025
+ }
1026
+ return accounts
1027
+ }
1028
+ if (platform === 'linux') {
1029
+ const { stdout, stderr, error } = await execCommand('secret-tool', ['search', '--all', '--unlock', 'service', service])
1030
+ if (error) return []
1031
+ const combined = `${stdout}\n${stderr}`
1032
+ const accounts: string[] = []
1033
+ const matches = combined.matchAll(/\baccount = (.+)/g)
1034
+ for (const match of matches) if (match[1]) accounts.push(match[1])
1035
+ return accounts
1036
+ }
1037
+ throw new Error(`Unsupported platform: ${platform}`)
1038
+ },
1039
+ async get(): Promise<string | undefined> {
1040
+ const platform = os.platform()
1041
+ if (platform === 'darwin') {
1042
+ const { stdout, error } = await execCommand('security', ['find-generic-password', '-s', service, '-a', account, '-w'])
1043
+ return error ? undefined : stdout
1044
+ }
1045
+ if (platform === 'linux') {
1046
+ const { stdout, error } = await execCommand('secret-tool', ['lookup', 'service', service, 'account', account])
1047
+ return error ? undefined : stdout || undefined
1048
+ }
1049
+ throw new Error(`Unsupported platform: ${platform}`)
1050
+ },
1051
+ async set(value: string): Promise<void> {
1052
+ const platform = os.platform()
1053
+ if (platform === 'darwin') {
1054
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
1055
+ const { error } = await execCommand('security', ['add-generic-password', '-s', service, '-a', account, '-w', value])
1056
+ if (error) throw error
1057
+ return
1058
+ }
1059
+ if (platform === 'linux') {
1060
+ const proc = child.execFile('secret-tool', ['store', '--label', `${service} ${account}`, 'service', service, 'account', account])
1061
+ proc.stdin?.write(value)
1062
+ proc.stdin?.end()
1063
+ return new Promise((resolve, reject) => {
1064
+ proc.on('close', (code) => {
1065
+ if (code === 0) resolve()
1066
+ else reject(new Error(`secret-tool exited with code ${code}`))
1067
+ })
1068
+ proc.on('error', reject)
1069
+ })
1070
+ }
1071
+ throw new Error(`Unsupported platform: ${platform}`)
1072
+ },
1073
+ async delete(): Promise<void> {
1074
+ const platform = os.platform()
1075
+ if (platform === 'darwin') {
1076
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
1077
+ return
1078
+ }
1079
+ if (platform === 'linux') {
1080
+ await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
1081
+ return
1082
+ }
1083
+ throw new Error(`Unsupported platform: ${platform}`)
1084
+ },
1085
+ }
1086
+ }
1087
+
1088
+ function prompt(message: string): Promise<string | undefined> {
1089
+ const reader = readline.createInterface({ input: process.stdin, output: process.stderr })
1090
+ return new Promise((resolve) => {
1091
+ reader.on('close', () => resolve(undefined))
1092
+ reader.question(`${pc.bold(`▸ ${message}:`)} `, (answer) => {
1093
+ reader.close()
1094
+ const value = answer.trim()
1095
+ resolve(value || undefined)
1096
+ })
1097
+ })
1098
+ }
1099
+
1100
+ function confirm(prompt: string): Promise<boolean> {
1101
+ const reader = readline.createInterface({ input: process.stdin, output: process.stderr })
1102
+ return new Promise((resolve) => {
1103
+ reader.question(`${pc.bold(`▸ ${prompt}`)} ${pc.dim('(y/N)')} `, (answer) => {
1104
+ reader.close()
1105
+ resolve(answer.trim().toLowerCase() === 'y')
1106
+ })
1107
+ })
1108
+ }
1109
+
1110
+ // Inlined from https://github.com/alexeyraspopov/picocolors (ISC License)
1111
+ const pc = (() => {
1112
+ const p = process || ({} as NodeJS.Process)
1113
+ const argv = p.argv || []
1114
+ const env = p.env || {}
1115
+ const isColorSupported =
1116
+ !(!!env.NO_COLOR || argv.includes('--no-color')) &&
1117
+ (!!env.FORCE_COLOR ||
1118
+ argv.includes('--color') ||
1119
+ ((p.stdout || ({} as NodeJS.WriteStream)).isTTY && env.TERM !== 'dumb') ||
1120
+ !!env.CI)
1121
+
1122
+ const replaceClose = (string: string, close: string, replace: string, index: number): string => {
1123
+ let result = ''
1124
+ let cursor = 0
1125
+ let i = index
1126
+ do {
1127
+ result += string.substring(cursor, i) + replace
1128
+ cursor = i + close.length
1129
+ i = string.indexOf(close, cursor)
1130
+ } while (~i)
1131
+ return result + string.substring(cursor)
1132
+ }
1133
+
1134
+ const formatter =
1135
+ (open: string, close: string, replace = open) =>
1136
+ (input: unknown) => {
1137
+ const string = `${input}`
1138
+ const index = string.indexOf(close, open.length)
1139
+ return ~index
1140
+ ? open + replaceClose(string, close, replace, index) + close
1141
+ : open + string + close
1142
+ }
1143
+
1144
+ const f = isColorSupported ? formatter : () => String
1145
+ return {
1146
+ isColorSupported,
1147
+ reset: f('\x1b[0m', '\x1b[0m'),
1148
+ bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
1149
+ dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
1150
+ italic: f('\x1b[3m', '\x1b[23m'),
1151
+ underline: f('\x1b[4m', '\x1b[24m'),
1152
+ inverse: f('\x1b[7m', '\x1b[27m'),
1153
+ hidden: f('\x1b[8m', '\x1b[28m'),
1154
+ strikethrough: f('\x1b[9m', '\x1b[29m'),
1155
+ black: f('\x1b[30m', '\x1b[39m'),
1156
+ red: f('\x1b[31m', '\x1b[39m'),
1157
+ green: f('\x1b[32m', '\x1b[39m'),
1158
+ yellow: f('\x1b[33m', '\x1b[39m'),
1159
+ blue: f('\x1b[34m', '\x1b[39m'),
1160
+ magenta: f('\x1b[35m', '\x1b[39m'),
1161
+ cyan: f('\x1b[36m', '\x1b[39m'),
1162
+ white: f('\x1b[37m', '\x1b[39m'),
1163
+ gray: f('\x1b[90m', '\x1b[39m'),
1164
+ bgBlack: f('\x1b[40m', '\x1b[49m'),
1165
+ bgRed: f('\x1b[41m', '\x1b[49m'),
1166
+ bgGreen: f('\x1b[42m', '\x1b[49m'),
1167
+ bgYellow: f('\x1b[43m', '\x1b[49m'),
1168
+ bgBlue: f('\x1b[44m', '\x1b[49m'),
1169
+ bgMagenta: f('\x1b[45m', '\x1b[49m'),
1170
+ bgCyan: f('\x1b[46m', '\x1b[49m'),
1171
+ bgWhite: f('\x1b[47m', '\x1b[49m'),
1172
+ blackBright: f('\x1b[90m', '\x1b[39m'),
1173
+ redBright: f('\x1b[91m', '\x1b[39m'),
1174
+ greenBright: f('\x1b[92m', '\x1b[39m'),
1175
+ yellowBright: f('\x1b[93m', '\x1b[39m'),
1176
+ blueBright: f('\x1b[94m', '\x1b[39m'),
1177
+ magentaBright: f('\x1b[95m', '\x1b[39m'),
1178
+ cyanBright: f('\x1b[96m', '\x1b[39m'),
1179
+ whiteBright: f('\x1b[97m', '\x1b[39m'),
1180
+ bgBlackBright: f('\x1b[100m', '\x1b[49m'),
1181
+ bgRedBright: f('\x1b[101m', '\x1b[49m'),
1182
+ bgGreenBright: f('\x1b[102m', '\x1b[49m'),
1183
+ bgYellowBright: f('\x1b[103m', '\x1b[49m'),
1184
+ bgBlueBright: f('\x1b[104m', '\x1b[49m'),
1185
+ bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
1186
+ bgCyanBright: f('\x1b[106m', '\x1b[49m'),
1187
+ bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
1188
+ link(url: string, text: string) {
1189
+ if (!isColorSupported) return text
1190
+ return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`
1191
+ },
1192
+ }
1193
+ })()
1194
+
1195
+ async function resolveChain(opts: { rpcUrl?: string | undefined } = {}): Promise<Chain> {
1196
+ if (!opts.rpcUrl) return tempoModerato
1197
+ const { getChainId } = await import('viem/actions')
1198
+ const chainId = await getChainId(createClient({ transport: http(opts.rpcUrl) }))
1199
+ const allExports = Object.values(await import('viem/chains')) as unknown[]
1200
+ const candidates = allExports.filter(
1201
+ (c): c is Chain =>
1202
+ typeof c === 'object' && c !== null && 'id' in c && (c as Chain).id === chainId,
1203
+ )
1204
+ const found = candidates.find((c) => 'serializers' in c && c.serializers) ?? candidates[0]
1205
+ if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${opts.rpcUrl}`)
1206
+ return found
1207
+ }
1208
+
1209
+ function chainName(chain: { id: number; name: string }) {
1210
+ const chainNames: Record<number, string> = {
1211
+ [tempoMainnet.id]: 'mainnet',
1212
+ [tempoModerato.id]: 'testnet',
1213
+ }
1214
+ return chainNames[chain.id] ?? chain.name
1215
+ }
1216
+
1217
+ const pathUsd = '0x20c0000000000000000000000000000000000000' as Address
1218
+ const testnetTokens = [
1219
+ '0x20c0000000000000000000000000000000000000',
1220
+ '0x20c0000000000000000000000000000000000001',
1221
+ '0x20c0000000000000000000000000000000000002',
1222
+ '0x20c0000000000000000000000000000000000003',
1223
+ ] as const
1224
+
1225
+ function fmtBalance(b: bigint, symbol: string, decimals = 6) {
1226
+ const value = Number(b) / 10 ** decimals
1227
+ const [int, dec] = value.toString().split('.')
1228
+ const formatted = int!.replace(/\B(?=(\d{3})+(?!\d))/g, '_')
1229
+ return `${dec ? `${formatted}.${dec}` : formatted} ${pc.dim(symbol)}`
1230
+ }
1231
+
1232
+ function isTestnet(chain: Chain) {
1233
+ return chain.id !== tempoMainnet.id
1234
+ }
1235
+
1236
+ async function fetchTokenInfo(
1237
+ client: ReturnType<typeof createClient>,
1238
+ token: Address,
1239
+ account: Address,
1240
+ ) {
1241
+ const { Actions } = await import('viem/tempo')
1242
+ const [balance, metadata] = await Promise.all([
1243
+ Actions.token.getBalance(client, { account, token }).catch(() => 0n),
1244
+ Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token as string })),
1245
+ ])
1246
+ const symbol = token === pathUsd ? 'PathUSD' : metadata.symbol
1247
+ const decimals = 'decimals' in metadata ? metadata.decimals : 6
1248
+ return { balance, symbol, decimals }
1249
+ }
1250
+
1251
+ function detectTerminalBg(
1252
+ timeoutMs = 100,
1253
+ ): Promise<{ r: number; g: number; b: number } | undefined> {
1254
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return Promise.resolve(undefined)
1255
+ return new Promise((resolve) => {
1256
+ const wasRaw = process.stdin.isRaw
1257
+ let buf = ''
1258
+ const cleanup = () => {
1259
+ clearTimeout(timer)
1260
+ process.stdin.removeListener('data', onData)
1261
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false)
1262
+ process.stdin.pause()
1263
+ }
1264
+ const timer = setTimeout(() => {
1265
+ cleanup()
1266
+ resolve(undefined)
1267
+ }, timeoutMs)
1268
+ const onData = (data: Buffer) => {
1269
+ buf += data.toString()
1270
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence for terminal background detection
1271
+ const match = buf.match(/\x1b\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i)
1272
+ if (!match) return
1273
+ cleanup()
1274
+ const parse = (hex: string) => Number.parseInt(hex.slice(0, 2), 16)
1275
+ resolve({ r: parse(match[1]!), g: parse(match[2]!), b: parse(match[3]!) })
1276
+ }
1277
+ process.stdin.setRawMode(true)
1278
+ process.stdin.resume()
1279
+ process.stdin.on('data', onData)
1280
+ process.stdout.write('\x1b]11;?\x07')
1281
+ })
1282
+ }
1283
+
1284
+ async function fetchBalanceLines(
1285
+ address: Address,
1286
+ opts?: { chain?: Chain; rpcUrl?: string; includeTestnet?: boolean },
1287
+ ): Promise<string[]> {
1288
+ if (opts?.chain) {
1289
+ const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) })
1290
+ const label = pc.dim(`(${chainName(opts.chain)})`)
1291
+ if (isTestnet(opts.chain)) {
1292
+ const results = await Promise.all(
1293
+ testnetTokens.map((token) => fetchTokenInfo(client, token, address)),
1294
+ )
1295
+ return results
1296
+ .filter((t) => t.balance > 0n)
1297
+ .map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`)
1298
+ }
1299
+ const { balance, symbol, decimals } = await fetchTokenInfo(client, pathUsd, address)
1300
+ return [`${fmtBalance(balance, symbol, decimals)} ${label}`]
1301
+ }
1302
+
1303
+ const mainnetClient = createClient({ chain: tempoMainnet, transport: http() })
1304
+ const mainnetInfo = await fetchTokenInfo(mainnetClient, pathUsd, address)
1305
+ const lines = [fmtBalance(mainnetInfo.balance, mainnetInfo.symbol, mainnetInfo.decimals)]
1306
+
1307
+ if (opts?.includeTestnet !== false) {
1308
+ const testnetClient = createClient({ chain: tempoModerato, transport: http() })
1309
+ const testnetResults = await Promise.all(
1310
+ testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)),
1311
+ )
1312
+ for (const t of testnetResults) {
1313
+ if (t.balance > 0n)
1314
+ lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`)
1315
+ }
1316
+ }
1317
+
1318
+ return lines
1319
+ }