dexterm-cli 0.0.0-stage → 0.1.1

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 (349) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +379 -0
  4. package/dist/packages/agents/src/executor.js +177 -0
  5. package/dist/packages/agents/src/gate.js +176 -0
  6. package/dist/packages/agents/src/identity.js +179 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +114 -0
  12. package/dist/packages/agents/src/tools.js +69 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +303 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/analyst/backend.js +526 -0
  28. package/dist/packages/cli/src/analyst/results.js +38 -0
  29. package/dist/packages/cli/src/analyst/session.js +319 -0
  30. package/dist/packages/cli/src/audit.js +51 -0
  31. package/dist/packages/cli/src/backtest-cmd.js +149 -0
  32. package/dist/packages/cli/src/boot-state.js +62 -0
  33. package/dist/packages/cli/src/custom.js +261 -0
  34. package/dist/packages/cli/src/display.js +191 -0
  35. package/dist/packages/cli/src/doctor.js +361 -0
  36. package/dist/packages/cli/src/feed.js +634 -0
  37. package/dist/packages/cli/src/fixtures.js +275 -0
  38. package/dist/packages/cli/src/follow-cmd.js +221 -0
  39. package/dist/packages/cli/src/images-config.js +59 -0
  40. package/dist/packages/cli/src/index.js +31 -0
  41. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  42. package/dist/packages/cli/src/larp-record.js +21 -0
  43. package/dist/packages/cli/src/live/copy.js +80 -0
  44. package/dist/packages/cli/src/live/flow.js +233 -0
  45. package/dist/packages/cli/src/live/index.js +509 -0
  46. package/dist/packages/cli/src/live/intel.js +227 -0
  47. package/dist/packages/cli/src/live/newpairs.js +334 -0
  48. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  49. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  50. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  51. package/dist/packages/cli/src/live/token.js +585 -0
  52. package/dist/packages/cli/src/live/trade.js +929 -0
  53. package/dist/packages/cli/src/live/watchlist.js +178 -0
  54. package/dist/packages/cli/src/main.js +5583 -0
  55. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  56. package/dist/packages/cli/src/pack-cmd.js +323 -0
  57. package/dist/packages/cli/src/plan-cmd.js +278 -0
  58. package/dist/packages/cli/src/popup.js +810 -0
  59. package/dist/packages/cli/src/rules-cmd.js +131 -0
  60. package/dist/packages/cli/src/rules.js +339 -0
  61. package/dist/packages/cli/src/scan/command.js +57 -0
  62. package/dist/packages/cli/src/scan/engine.js +419 -0
  63. package/dist/packages/cli/src/scan/observe.js +64 -0
  64. package/dist/packages/cli/src/scan/ui.js +98 -0
  65. package/dist/packages/cli/src/screener/filter.js +241 -0
  66. package/dist/packages/cli/src/screener/presets.js +80 -0
  67. package/dist/packages/cli/src/screener/rows.js +146 -0
  68. package/dist/packages/cli/src/screener/rug.js +50 -0
  69. package/dist/packages/cli/src/screener/sort.js +52 -0
  70. package/dist/packages/cli/src/screener/view.js +183 -0
  71. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  72. package/dist/packages/cli/src/screens/analyst.js +326 -0
  73. package/dist/packages/cli/src/screens/boot.js +74 -0
  74. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  75. package/dist/packages/cli/src/screens/coin.js +33 -0
  76. package/dist/packages/cli/src/screens/follows.js +201 -0
  77. package/dist/packages/cli/src/screens/kol.js +90 -0
  78. package/dist/packages/cli/src/screens/narrative.js +90 -0
  79. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  80. package/dist/packages/cli/src/screens/overlay.js +257 -0
  81. package/dist/packages/cli/src/screens/plans.js +126 -0
  82. package/dist/packages/cli/src/screens/positions.js +176 -0
  83. package/dist/packages/cli/src/screens/rules.js +170 -0
  84. package/dist/packages/cli/src/screens/screener.js +459 -0
  85. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  86. package/dist/packages/cli/src/screens/ticket.js +342 -0
  87. package/dist/packages/cli/src/screens/token.js +312 -0
  88. package/dist/packages/cli/src/screens/trending.js +181 -0
  89. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  90. package/dist/packages/cli/src/social/demo.js +96 -0
  91. package/dist/packages/cli/src/social/view.js +250 -0
  92. package/dist/packages/cli/src/state.js +463 -0
  93. package/dist/packages/cli/src/theme-cmd.js +126 -0
  94. package/dist/packages/cli/src/trade.js +319 -0
  95. package/dist/packages/cli/src/voice-hud.js +42 -0
  96. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  97. package/dist/packages/cli/src/wallet-command.js +31 -0
  98. package/dist/packages/cli/src/widgets.js +208 -0
  99. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  100. package/dist/packages/cli/src/x-cmd.js +154 -0
  101. package/dist/packages/custom/src/config.js +213 -0
  102. package/dist/packages/custom/src/index.js +10 -0
  103. package/dist/packages/custom/src/keymap.js +107 -0
  104. package/dist/packages/custom/src/layout.js +235 -0
  105. package/dist/packages/custom/src/packs.js +633 -0
  106. package/dist/packages/custom/src/rules.js +704 -0
  107. package/dist/packages/custom/src/schema.js +515 -0
  108. package/dist/packages/custom/src/themes.js +176 -0
  109. package/dist/packages/custom/src/widget-html.js +222 -0
  110. package/dist/packages/custom/src/workspace.js +141 -0
  111. package/dist/packages/data/src/analyst-actions.js +133 -0
  112. package/dist/packages/data/src/budget.js +88 -0
  113. package/dist/packages/data/src/cache.js +142 -0
  114. package/dist/packages/data/src/candles.js +168 -0
  115. package/dist/packages/data/src/clock.js +64 -0
  116. package/dist/packages/data/src/credits.js +137 -0
  117. package/dist/packages/data/src/detail.js +97 -0
  118. package/dist/packages/data/src/fast-lane.js +321 -0
  119. package/dist/packages/data/src/fixtures.js +135 -0
  120. package/dist/packages/data/src/health.js +245 -0
  121. package/dist/packages/data/src/http.js +277 -0
  122. package/dist/packages/data/src/images/cache.js +141 -0
  123. package/dist/packages/data/src/images/decode-worker.js +28 -0
  124. package/dist/packages/data/src/images/decode.js +81 -0
  125. package/dist/packages/data/src/images/fetch.js +162 -0
  126. package/dist/packages/data/src/images/gif.js +214 -0
  127. package/dist/packages/data/src/images/index.js +10 -0
  128. package/dist/packages/data/src/images/jpeg.js +544 -0
  129. package/dist/packages/data/src/images/png.js +310 -0
  130. package/dist/packages/data/src/images/service.js +334 -0
  131. package/dist/packages/data/src/images/types.js +27 -0
  132. package/dist/packages/data/src/index.js +208 -0
  133. package/dist/packages/data/src/intel/helius-history.js +59 -0
  134. package/dist/packages/data/src/intel/holder-class.js +125 -0
  135. package/dist/packages/data/src/intel/rules.js +421 -0
  136. package/dist/packages/data/src/intel/scan.js +170 -0
  137. package/dist/packages/data/src/larp/evidence.js +288 -0
  138. package/dist/packages/data/src/larp/links.js +137 -0
  139. package/dist/packages/data/src/larp/robots.js +90 -0
  140. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  141. package/dist/packages/data/src/limiter.js +341 -0
  142. package/dist/packages/data/src/model.js +12 -0
  143. package/dist/packages/data/src/paper.js +331 -0
  144. package/dist/packages/data/src/rpc-config.js +126 -0
  145. package/dist/packages/data/src/rpc-pool.js +166 -0
  146. package/dist/packages/data/src/social/chain.js +83 -0
  147. package/dist/packages/data/src/social/dedupe.js +205 -0
  148. package/dist/packages/data/src/social/feed.js +462 -0
  149. package/dist/packages/data/src/social/firewall.js +110 -0
  150. package/dist/packages/data/src/social/index.js +11 -0
  151. package/dist/packages/data/src/social/kol.js +269 -0
  152. package/dist/packages/data/src/social/model.js +11 -0
  153. package/dist/packages/data/src/social/rank.js +184 -0
  154. package/dist/packages/data/src/social/rss.js +113 -0
  155. package/dist/packages/data/src/social/summary.js +104 -0
  156. package/dist/packages/data/src/social/venues.js +186 -0
  157. package/dist/packages/data/src/sources/dexscreener.js +170 -0
  158. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  159. package/dist/packages/data/src/sources/goplus.js +161 -0
  160. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  161. package/dist/packages/data/src/sources/jupiter.js +198 -0
  162. package/dist/packages/data/src/sources/pda.js +113 -0
  163. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  164. package/dist/packages/data/src/sources/pumpportal.js +247 -0
  165. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  166. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  167. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  168. package/dist/packages/data/src/store.js +289 -0
  169. package/dist/packages/data/src/trade/bridge.js +464 -0
  170. package/dist/packages/data/src/trade/caps.js +323 -0
  171. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  172. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  173. package/dist/packages/data/src/trade/copy/index.js +9 -0
  174. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  175. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  176. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  177. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  178. package/dist/packages/data/src/trade/copy/types.js +6 -0
  179. package/dist/packages/data/src/trade/executor.js +933 -0
  180. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  181. package/dist/packages/data/src/trade/link-store.js +52 -0
  182. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  183. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  184. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  185. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  186. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  187. package/dist/packages/data/src/trade/plans/index.js +12 -0
  188. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  189. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  190. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  191. package/dist/packages/data/src/trade/plans/service.js +168 -0
  192. package/dist/packages/data/src/trade/plans/types.js +6 -0
  193. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  194. package/dist/packages/data/src/trade/signer/client.js +197 -0
  195. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  196. package/dist/packages/data/src/trade/signer/host.js +97 -0
  197. package/dist/packages/data/src/trade/signer/main.js +121 -0
  198. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  199. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  200. package/dist/packages/data/src/trade/signer/service.js +400 -0
  201. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  202. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  203. package/dist/packages/data/src/trade/swap-build.js +108 -0
  204. package/dist/packages/data/src/trade/token-delta.js +201 -0
  205. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  206. package/dist/packages/data/src/util.js +24 -0
  207. package/dist/packages/data/src/watchlist.js +176 -0
  208. package/dist/packages/data/src/x/api.js +256 -0
  209. package/dist/packages/data/src/x/budget.js +66 -0
  210. package/dist/packages/data/src/x/config.js +49 -0
  211. package/dist/packages/data/src/x/index.js +10 -0
  212. package/dist/packages/data/src/x/mock-server.js +217 -0
  213. package/dist/packages/data/src/x/oauth.js +242 -0
  214. package/dist/packages/data/src/x/pkce.js +21 -0
  215. package/dist/packages/data/src/x/source.js +285 -0
  216. package/dist/packages/data/src/x/token-store.js +134 -0
  217. package/dist/packages/data/src/x/transport.js +71 -0
  218. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  219. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  220. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  221. package/dist/packages/mcp/assets/cases.json +1194 -0
  222. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  223. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  224. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  225. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  226. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  227. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  228. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  229. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  230. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  231. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  232. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  233. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  234. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  235. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  236. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  237. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  238. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  239. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  240. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  241. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  242. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  243. package/dist/packages/mcp/src/dossier.js +78 -0
  244. package/dist/packages/mcp/src/firewall.js +21 -0
  245. package/dist/packages/mcp/src/index.js +22 -0
  246. package/dist/packages/mcp/src/server.js +849 -0
  247. package/dist/packages/mcp/src/user-content.js +434 -0
  248. package/dist/packages/packs/src/backtest.js +183 -0
  249. package/dist/packages/packs/src/digest.js +95 -0
  250. package/dist/packages/packs/src/firewall.js +57 -0
  251. package/dist/packages/packs/src/index.js +19 -0
  252. package/dist/packages/packs/src/larp-eval.js +100 -0
  253. package/dist/packages/packs/src/narrative.js +309 -0
  254. package/dist/packages/packs/src/reason.js +135 -0
  255. package/dist/packages/packs/src/rug-model.js +137 -0
  256. package/dist/packages/packs/src/schema.js +54 -0
  257. package/dist/packages/packs/src/token-dossier.js +174 -0
  258. package/dist/packages/packs/src/verdicts.js +17 -0
  259. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  260. package/dist/packages/popup/src/config.js +71 -0
  261. package/dist/packages/popup/src/dock.js +271 -0
  262. package/dist/packages/popup/src/host.js +210 -0
  263. package/dist/packages/popup/src/index.js +12 -0
  264. package/dist/packages/popup/src/loopback.js +53 -0
  265. package/dist/packages/popup/src/open.js +183 -0
  266. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  267. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  268. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  269. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  270. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  271. package/dist/packages/popup/src/pages/chart.css +50 -0
  272. package/dist/packages/popup/src/pages/chart.html +56 -0
  273. package/dist/packages/popup/src/pages/chart.js +554 -0
  274. package/dist/packages/popup/src/pages/common.css +34 -0
  275. package/dist/packages/popup/src/pages/common.js +187 -0
  276. package/dist/packages/popup/src/pages/feeds.html +20 -0
  277. package/dist/packages/popup/src/pages/feeds.js +77 -0
  278. package/dist/packages/popup/src/pages/narrative.html +20 -0
  279. package/dist/packages/popup/src/pages/narrative.js +61 -0
  280. package/dist/packages/popup/src/pages/plans.css +30 -0
  281. package/dist/packages/popup/src/pages/plans.html +26 -0
  282. package/dist/packages/popup/src/pages/plans.js +82 -0
  283. package/dist/packages/popup/src/pages/roster.css +41 -0
  284. package/dist/packages/popup/src/pages/roster.html +25 -0
  285. package/dist/packages/popup/src/pages/roster.js +75 -0
  286. package/dist/packages/popup/src/pages/token.html +20 -0
  287. package/dist/packages/popup/src/pages/token.js +112 -0
  288. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  289. package/dist/packages/popup/src/pages/widget.css +5 -0
  290. package/dist/packages/popup/src/pages/widget.html +21 -0
  291. package/dist/packages/popup/src/pages/widget.js +85 -0
  292. package/dist/packages/popup/src/protocol.js +31 -0
  293. package/dist/packages/popup/src/selftest.js +156 -0
  294. package/dist/packages/popup/src/server.js +582 -0
  295. package/dist/packages/popup/src/sinkproxy.js +167 -0
  296. package/dist/packages/popup/src/widget-html.js +231 -0
  297. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  298. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  299. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  300. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  301. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  302. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  303. package/dist/packages/recorder/src/cli.js +113 -0
  304. package/dist/packages/recorder/src/export.js +71 -0
  305. package/dist/packages/recorder/src/index.js +20 -0
  306. package/dist/packages/recorder/src/labels.js +151 -0
  307. package/dist/packages/recorder/src/open.js +29 -0
  308. package/dist/packages/recorder/src/recorder.js +553 -0
  309. package/dist/packages/recorder/src/schedule.js +43 -0
  310. package/dist/packages/recorder/src/schema.js +135 -0
  311. package/dist/packages/recorder/src/snapshot.js +191 -0
  312. package/dist/packages/recorder/src/sweep.js +52 -0
  313. package/dist/packages/recorder/src/verdicts.js +52 -0
  314. package/dist/packages/tui/src/bubbles.js +415 -0
  315. package/dist/packages/tui/src/canvas.js +370 -0
  316. package/dist/packages/tui/src/chrome.js +85 -0
  317. package/dist/packages/tui/src/color.js +133 -0
  318. package/dist/packages/tui/src/flush.js +62 -0
  319. package/dist/packages/tui/src/fmt.js +188 -0
  320. package/dist/packages/tui/src/fx-trade.js +278 -0
  321. package/dist/packages/tui/src/fx.js +527 -0
  322. package/dist/packages/tui/src/images/blocks.js +141 -0
  323. package/dist/packages/tui/src/images/coin.js +22 -0
  324. package/dist/packages/tui/src/images/detect.js +140 -0
  325. package/dist/packages/tui/src/images/index.js +7 -0
  326. package/dist/packages/tui/src/images/layer.js +146 -0
  327. package/dist/packages/tui/src/images/pixels.js +157 -0
  328. package/dist/packages/tui/src/images/protocols.js +87 -0
  329. package/dist/packages/tui/src/index.js +15 -0
  330. package/dist/packages/tui/src/keymap.js +355 -0
  331. package/dist/packages/tui/src/layout.js +11 -0
  332. package/dist/packages/tui/src/sound.js +177 -0
  333. package/dist/packages/tui/src/theme.js +182 -0
  334. package/dist/packages/tui/src/widgets.js +598 -0
  335. package/dist/packages/voice/src/controller.js +395 -0
  336. package/dist/packages/voice/src/doctor.js +63 -0
  337. package/dist/packages/voice/src/engines.js +44 -0
  338. package/dist/packages/voice/src/index.js +16 -0
  339. package/dist/packages/voice/src/router.js +502 -0
  340. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  341. package/dist/packages/voice/src/stt/apple.js +179 -0
  342. package/dist/packages/voice/src/stt/fake.js +34 -0
  343. package/dist/packages/voice/src/stt/whisper.js +102 -0
  344. package/dist/packages/voice/src/summary.js +44 -0
  345. package/dist/packages/voice/src/tts/fake.js +30 -0
  346. package/dist/packages/voice/src/tts/say.js +96 -0
  347. package/dist/packages/voice/src/types.js +3 -0
  348. package/package.json +42 -4
  349. package/README.md +0 -3
@@ -0,0 +1,464 @@
1
+ // dexterm T1 — local loopback wallet bridge (SPEC §6.5 Phantom bridge; extends the proven task-1290
2
+ // connect/sign round trip in research/popup-host). Phantom is a browser extension, so dexterm links a
3
+ // wallet by opening a tiny page in the user's browser that talks to window.solana and POSTs a RESULT
4
+ // back to a one-shot loopback server. dexterm never sees a private key, seed, or signing material
5
+ // beyond the proof the user produced.
6
+ //
7
+ // Security posture (mirrors research/popup-host/server/server.mjs §5):
8
+ // * listens on 127.0.0.1 only; never 0.0.0.0 / ::. task-1368 (g): on a FIXED preferred port
9
+ // (`WALLET_BRIDGE_PORT`, random only when it is busy) — Phantom remembers trust PER ORIGIN, and a
10
+ // random port made every page a brand-new origin: the sign page then called signTransaction on an
11
+ // origin Phantom had never connected and got 4100 ("not authorized"). The sign page now connects
12
+ // first and checks the account; the stable origin lets that connect resolve silently next time.
13
+ // * a single-use, random 32-byte TOKEN in the URL; a second use is refused (replay-proof).
14
+ // * strict Host check on every request; strict Origin + JSON content-type + 16 KB body cap on POST.
15
+ // * remote address must be loopback; anything else gets an empty 404.
16
+ // * all JS/CSS inlined under a CSP nonce; CSP forbids remote content, inline handlers, eval, images.
17
+ // * the server auto-closes after the first valid result OR after `timeoutMs` (default 120 s).
18
+ // * the bridge is opened through the small `openBridge` interface so a popup-host lane can later swap
19
+ // the default browser for a chromeless window WITHOUT this module depending on wry/tao/Electron.
20
+ //
21
+ // Crypto: node:crypto Ed25519 verify of the link proof (raw 32-byte pubkey -> SPKI DER). No key is
22
+ // generated, stored or logged here; dexterm keeps only the PUBLIC key.
23
+ import http from 'node:http';
24
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
25
+ import { spawn } from 'node:child_process';
26
+ import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { join } from 'node:path';
29
+ import { systemClock } from "../clock.js";
30
+ import { base58 } from "../sources/solana-rpc.js";
31
+ import { verifyEd25519 } from "./tx-decode.js";
32
+ const TOKEN_TTL_MS = 120_000;
33
+ /**
34
+ * task-1328: the tx-sign page needs @solana/web3.js in the BROWSER (Phantom's signTransaction takes a
35
+ * VersionedTransaction object). The vendored IIFE bundle (packages/data/vendor/solana-web3, MIT,
36
+ * sha256-pinned by the tests) is inlined into the page under the CSP nonce — no CDN, no remote script.
37
+ * Read lazily and cached; a missing file leaves the page in its "needs the shell" state (closed).
38
+ */
39
+ export const SOLANA_WEB3_VENDOR = new URL('../../vendor/solana-web3/index.iife.min.js', import.meta.url);
40
+ let web3Bundle;
41
+ export function solanaWeb3Bundle() {
42
+ if (web3Bundle !== undefined)
43
+ return web3Bundle;
44
+ try {
45
+ web3Bundle = readFileSync(SOLANA_WEB3_VENDOR, 'utf8');
46
+ }
47
+ catch {
48
+ web3Bundle = null;
49
+ }
50
+ return web3Bundle;
51
+ }
52
+ const MAX_BODY = 16 * 1024;
53
+ const LOOPBACK = new Set(['127.0.0.1', '::ffff:127.0.0.1', '::1']);
54
+ function spawnOpener(target) {
55
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
56
+ const args = process.platform === 'win32' ? ['/c', 'start', '', target] : [target];
57
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
58
+ child.on('error', () => { });
59
+ child.unref();
60
+ }
61
+ /**
62
+ * Open the bridge URL in the user's default browser. No wry/Electron dependency; keeps the bridge
63
+ * portable. Round 2 (re-verify LOW): the single-use token is NOT passed on the `open` command line
64
+ * (argv is readable by every local process via `ps`). Instead a mode-600 launcher page in a mode-700
65
+ * temp dir redirects the browser to the URL; the file is removed shortly after. Set
66
+ * `DEXTERM_BRIDGE_OPEN=argv` to fall back to passing the URL directly if a platform's default handler
67
+ * for `.html` is not the browser.
68
+ */
69
+ export const systemBrowserOpener = (url) => {
70
+ if (process.env.DEXTERM_BRIDGE_OPEN === 'argv') {
71
+ spawnOpener(url);
72
+ return;
73
+ }
74
+ let dir;
75
+ try {
76
+ dir = mkdtempSync(join(tmpdir(), 'dexterm-bridge-')); // mkdtemp creates the dir mode 700
77
+ const page = `<!doctype html><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${esc(url)}">` +
78
+ `<title>dexterm</title><script>location.replace(${JSON.stringify(url)})</script>` +
79
+ `<p>Opening dexterm… <a href="${esc(url)}">continue</a></p>`;
80
+ writeFileSync(join(dir, 'open.html'), page, { mode: 0o600 });
81
+ }
82
+ catch {
83
+ spawnOpener(url); // could not stage a launcher; the argv path still works
84
+ return;
85
+ }
86
+ spawnOpener(join(dir, 'open.html'));
87
+ const t = setTimeout(() => { try {
88
+ rmSync(dir, { recursive: true, force: true });
89
+ }
90
+ catch { /* best effort */ } }, 20_000);
91
+ if (typeof t.unref === 'function')
92
+ t.unref();
93
+ };
94
+ /**
95
+ * The wallet bridge's preferred loopback port: `http://127.0.0.1:41291` is the ORIGIN Phantom stores
96
+ * its trust for (link and sign share it). Unassigned by IANA; the X login callback is 41287.
97
+ */
98
+ export const WALLET_BRIDGE_PORT = 41291;
99
+ function b64url(buf) { return buf.toString('base64url'); }
100
+ function safeEq(a, b) {
101
+ const ba = Buffer.from(a), bb = Buffer.from(b);
102
+ return ba.length === bb.length && timingSafeEqual(ba, bb);
103
+ }
104
+ /** Only a STRING is ever read off the untrusted body; anything else is the empty string (round 2 MEDIUM #3). */
105
+ function str(v) { return typeof v === 'string' ? v : ''; }
106
+ function sanitize(s, n = 80) { return str(s).replace(/[^\x20-\x7e]/g, '?').slice(0, n); }
107
+ const CSP = (port, nonce) => `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; ` +
108
+ `connect-src http://127.0.0.1:${port}; img-src 'none'; font-src 'none'; base-uri 'none'; ` +
109
+ `form-action 'none'; frame-ancestors 'none'; object-src 'none'`;
110
+ const SEC_HEADERS = (port, nonce) => ({
111
+ 'content-type': 'text/html; charset=utf-8',
112
+ 'cache-control': 'no-store',
113
+ 'referrer-policy': 'no-referrer',
114
+ 'x-content-type-options': 'nosniff',
115
+ 'cross-origin-opener-policy': 'same-origin',
116
+ 'cross-origin-resource-policy': 'same-origin',
117
+ 'permissions-policy': 'camera=(), microphone=(), geolocation=(), payment=(), usb=()',
118
+ 'content-security-policy': CSP(port, nonce),
119
+ });
120
+ /** Escape untrusted values interpolated into the page HTML. */
121
+ function esc(s) {
122
+ return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
123
+ }
124
+ /**
125
+ * A one-shot loopback exchange: serve ONE page at `GET /<route>?t=<token>` and accept ONE result at
126
+ * `POST /<route>/result`, then resolve and close. The page HTML is built with the CSP nonce. The
127
+ * POST handler returns `{ done, error }`: `done:true` resolves the outer promise.
128
+ */
129
+ function oneShot(args) {
130
+ return new Promise((resolve, reject) => {
131
+ const token = b64url(randomBytes(32));
132
+ let port = 0;
133
+ let fellBack = false; // the preferred port was busy; listening on a random one instead
134
+ let used = false; // the page token is single-use
135
+ let resultTaken = false; // the POST result is single-use (replay-proof)
136
+ let settled = false;
137
+ const finish = (fn) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ clearTimeout(timer);
142
+ // task-1368 (g): with a FIXED port the next exchange is the SAME origin, and a browser (or node's
143
+ // fetch) happily reuses a keep-alive connection to THIS server for it — which would answer 404
144
+ // (unknown token) for the next page. Close the listener AND every connection, so the next page
145
+ // can only ever reach the next server. (Same lesson as the X login callback server, task-1357.)
146
+ try {
147
+ server.close();
148
+ server.closeAllConnections();
149
+ }
150
+ catch { /* ignore */ }
151
+ fn();
152
+ };
153
+ const reject404 = (res) => { res.writeHead(404, { 'content-length': '0', 'cache-control': 'no-store' }); res.end(); };
154
+ const hostOk = (req) => req.headers.host === `127.0.0.1:${port}`;
155
+ const loopbackOk = (req) => LOOPBACK.has(req.socket.remoteAddress ?? '');
156
+ const originOk = (req) => req.headers.origin === `http://127.0.0.1:${port}`;
157
+ const readBody = (req) => new Promise((res2, rej2) => {
158
+ let size = 0;
159
+ const chunks = [];
160
+ req.on('data', (c) => { size += c.length; if (size > MAX_BODY) {
161
+ rej2(new Error('body-too-large'));
162
+ req.destroy();
163
+ }
164
+ else
165
+ chunks.push(c); });
166
+ req.on('end', () => res2(Buffer.concat(chunks).toString('utf8')));
167
+ req.on('error', rej2);
168
+ });
169
+ const handle = async (req, res) => {
170
+ if (!loopbackOk(req) || !hostOk(req))
171
+ return reject404(res);
172
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
173
+ if (req.method === 'GET' && url.pathname === `/${args.route}`) {
174
+ const t = url.searchParams.get('t') ?? '';
175
+ if (!safeEq(t, token) || used)
176
+ return reject404(res);
177
+ used = true;
178
+ const nonce = b64url(randomBytes(16));
179
+ res.writeHead(200, SEC_HEADERS(port, nonce));
180
+ // task-1368 (g): tell the user on the page when this is NOT the usual origin — Phantom will
181
+ // ask to connect once more, and its stored trust for the fixed port is untouched.
182
+ const portNote = fellBack ? `the usual dexterm port 127.0.0.1:${args.preferredPort} was busy, so this page is on a one-off port (${port}). Phantom may ask you to connect once; that is expected.` : '';
183
+ res.end(args.page({ port, nonce, token, portNote }));
184
+ return;
185
+ }
186
+ if (req.method === 'POST' && url.pathname === `/${args.route}/result`) {
187
+ if (!originOk(req))
188
+ return reject404(res);
189
+ if (!/^application\/json/.test(req.headers['content-type'] ?? '')) {
190
+ res.writeHead(415, { 'content-length': '0' });
191
+ res.end();
192
+ return;
193
+ }
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(await readBody(req));
197
+ }
198
+ catch {
199
+ res.writeHead(400, { 'content-length': '0' });
200
+ res.end();
201
+ return;
202
+ }
203
+ // DEFECT 7: `JSON.parse('null')` (and `123`, `"x"`, `[]`) parse WITHOUT throwing; reading `.t` off
204
+ // them crashed the handler before the token check. Require a plain object.
205
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
206
+ res.writeHead(400, { 'content-length': '0' });
207
+ res.end();
208
+ return;
209
+ }
210
+ const body = parsed;
211
+ // Round 2 MEDIUM #3: never coerce the token field — `String({toString:1})` throws. Only a string
212
+ // can match; anything else is '' and fails the comparison.
213
+ if (!safeEq(str(body.t), token) || resultTaken) {
214
+ res.writeHead(403, { 'content-length': '0' });
215
+ res.end();
216
+ return;
217
+ }
218
+ // The exchange is single-use from here on: a malformed or badly-signed first result BURNS it
219
+ // (documented, accepted for round 2) and the user re-runs `dexterm wallet link`. This is the
220
+ // conservative choice — an attacker who knows the token gets exactly one attempt, never a retry.
221
+ resultTaken = true;
222
+ const out = args.onResult(body);
223
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
224
+ res.end(JSON.stringify({ ok: out.error === undefined, error: out.error ?? null }));
225
+ // Resolve (and close every connection) only once the page has its answer: `finish` now tears
226
+ // the socket down, and the page's "✓ signed" line reads this response.
227
+ if (out.error === undefined) {
228
+ const done = () => finish(() => resolve(out.value));
229
+ res.once('finish', done);
230
+ res.once('close', done);
231
+ }
232
+ return;
233
+ }
234
+ return reject404(res);
235
+ };
236
+ // Round 2 MEDIUM #3: NO request body (or header, or handler bug) may crash the process. Anything
237
+ // the handler throws — sync or async — is a 400 (or a dropped socket if headers already went out).
238
+ const server = http.createServer((req, res) => {
239
+ // task-1368 (g): a one-shot server wants NO keep-alive. With the fixed port the next exchange is
240
+ // the same origin, and a client that pooled this connection would race our close with its next
241
+ // request (a stale socket → "fetch failed", the page never loads). `Connection: close` on every
242
+ // response tells the browser (and node's fetch) up front: do not reuse this socket.
243
+ res.setHeader('connection', 'close');
244
+ handle(req, res).catch(() => {
245
+ try {
246
+ if (!res.headersSent) {
247
+ res.writeHead(400, { 'content-length': '0' });
248
+ res.end();
249
+ }
250
+ else
251
+ res.destroy();
252
+ }
253
+ catch { /* socket already gone */ }
254
+ });
255
+ });
256
+ server.on('clientError', (_err, socket) => { try {
257
+ socket.destroy();
258
+ }
259
+ catch { /* ignore */ } });
260
+ server.headersTimeout = 5000;
261
+ server.requestTimeout = 10_000;
262
+ server.keepAliveTimeout = 5000;
263
+ server.maxHeadersCount = 40;
264
+ const onListening = () => {
265
+ port = server.address().port;
266
+ const url = `http://127.0.0.1:${port}/${args.route}?t=${token}`;
267
+ void Promise.resolve(args.open(url)).catch(() => { });
268
+ };
269
+ // Registered ONCE, not as a listen() callback: a callback given to a listen() that fails with
270
+ // EADDRINUSE stays attached, and the retry would then fire it twice — the page URL opened twice,
271
+ // the second open burning the single-use token (found by the concurrent test files).
272
+ server.once('listening', onListening);
273
+ // task-1368 (g): the preferred port busy (another dexterm, a leftover bridge, anything) is not an
274
+ // error — listen again on a random port, exactly the pre-1368 behaviour. Any other error rejects.
275
+ server.on('error', (e) => {
276
+ if (!fellBack && args.preferredPort > 0 && (e.code === 'EADDRINUSE' || e.code === 'EACCES')) {
277
+ fellBack = true;
278
+ server.listen(0, '127.0.0.1');
279
+ return;
280
+ }
281
+ finish(() => reject(e));
282
+ });
283
+ const timer = setTimeout(() => finish(() => reject(new Error(`wallet bridge timed out after ${args.timeoutMs} ms`))), args.timeoutMs);
284
+ if (typeof timer.unref === 'function')
285
+ timer.unref();
286
+ server.listen(args.preferredPort, '127.0.0.1');
287
+ });
288
+ }
289
+ export class WalletBridge {
290
+ open;
291
+ timeoutMs;
292
+ clock;
293
+ port;
294
+ constructor(opts = {}) {
295
+ this.open = opts.open ?? systemBrowserOpener;
296
+ this.timeoutMs = opts.timeoutMs ?? TOKEN_TTL_MS;
297
+ this.clock = opts.clock ?? systemClock;
298
+ const p = opts.port ?? WALLET_BRIDGE_PORT;
299
+ this.port = Number.isInteger(p) && p >= 0 && p <= 65535 ? p : WALLET_BRIDGE_PORT;
300
+ }
301
+ /** The port the bridge asks for (the origin Phantom remembers); 0 means a random port every time. */
302
+ get preferredPort() { return this.port; }
303
+ /** The challenge a wallet signs to prove ownership. Human-readable, never a transaction. */
304
+ static challenge(nonce, port) {
305
+ return [
306
+ 'dexterm wants to link your Solana wallet.',
307
+ '',
308
+ 'This signs a message only. It does NOT approve a transaction, move funds, or cost any fees.',
309
+ '',
310
+ `URI: http://127.0.0.1:${port}`,
311
+ `Nonce: ${nonce}`,
312
+ `Issued At: ${new Date().toISOString()}`,
313
+ ].join('\n');
314
+ }
315
+ /**
316
+ * Link a wallet: open the browser to a page that connects window.solana and signs a dexterm nonce,
317
+ * verify the Ed25519 proof, and resolve with the PUBLIC key only. Never receives a private key.
318
+ */
319
+ link() {
320
+ const nonce = b64url(randomBytes(18));
321
+ const started = this.clock.now();
322
+ let servedChallenge = ''; // the exact text dexterm served; the signature must be over THIS.
323
+ return oneShot({
324
+ route: 'link',
325
+ open: this.open,
326
+ timeoutMs: this.timeoutMs,
327
+ clock: this.clock,
328
+ preferredPort: this.port,
329
+ page: ({ port, nonce: cspNonce, portNote }) => { servedChallenge = WalletBridge.challenge(nonce, port); return linkPage(port, cspNonce, servedChallenge, portNote); },
330
+ onResult: (body) => {
331
+ try {
332
+ const pk = Buffer.from(str(body.publicKey), 'base64');
333
+ const sig = Buffer.from(str(body.signature), 'base64');
334
+ // Verify against the server-held challenge, never the value the page echoed back.
335
+ const challenge = Buffer.from(servedChallenge, 'utf8');
336
+ if (servedChallenge === '' || !verifyEd25519(pk, challenge, sig))
337
+ return { error: 'signature did not verify' };
338
+ return {
339
+ value: {
340
+ publicKey: base58(pk),
341
+ publicKeyBase64: pk.toString('base64'),
342
+ provider: sanitize(body.provider ?? 'unknown', 24),
343
+ roundTripMs: this.clock.now() - started,
344
+ },
345
+ };
346
+ }
347
+ catch (e) {
348
+ return { error: `bad link result: ${sanitize(e instanceof Error ? e.message : e, 60)}` };
349
+ }
350
+ },
351
+ });
352
+ }
353
+ /**
354
+ * Hand an unsigned transaction to the wallet for signing and resolve with the signed bytes. dexterm
355
+ * never holds a key; the browser approval IS the physical confirmation (SPEC §6.5). The unsigned tx
356
+ * is the plan the user already confirmed; a popup-host lane can swap the page for a chromeless window.
357
+ */
358
+ signTransaction(args) {
359
+ const started = this.clock.now();
360
+ return oneShot({
361
+ route: 'sign',
362
+ open: this.open,
363
+ timeoutMs: this.timeoutMs,
364
+ clock: this.clock,
365
+ preferredPort: this.port,
366
+ page: ({ port, nonce: cspNonce, portNote }) => signPage(port, cspNonce, args.unsignedTxBase64, args.expectPublicKey ?? '', args.label ?? 'swap', portNote),
367
+ onResult: (body) => {
368
+ // task-1368 (g): the page reports which account signed (base58). When the caller named the
369
+ // linked wallet, a different account is refused HERE too — the page's own check is UX, this is
370
+ // the gate (the signer also verifies the signature over the bytes; this is belt and braces).
371
+ const account = str(body.publicKey);
372
+ if (args.expectPublicKey && account !== '' && account !== args.expectPublicKey)
373
+ return { error: `signed by a different wallet account (${sanitize(account, 8)}…), not the linked wallet` };
374
+ const signed = str(body.signedTx);
375
+ if (signed.length === 0)
376
+ return { error: 'wallet returned no signed transaction' };
377
+ if (signed.length > 4096)
378
+ return { error: 'signed transaction too large' };
379
+ if (!/^[A-Za-z0-9+/=]+$/.test(signed))
380
+ return { error: 'signed transaction is not base64' };
381
+ return { value: { signedTxBase64: signed, provider: sanitize(body.provider ?? 'unknown', 24), roundTripMs: this.clock.now() - started } };
382
+ },
383
+ });
384
+ }
385
+ }
386
+ // --------------------------------------------------------------------------- inlined browser pages
387
+ // These run in the user's browser (real Phantom). The fake-Phantom e2e POSTs the result directly, so
388
+ // the page JS is exercised only in the manual run (scripts/). Untrusted values are escaped; the CSP
389
+ // blocks everything remote.
390
+ function linkPage(port, nonce, challenge, portNote = '') {
391
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">
392
+ <meta name="viewport" content="width=device-width, initial-scale=1">
393
+ <title>dexterm · link wallet</title>
394
+ <style nonce="${nonce}">body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#07090d;color:#d7dbe0;margin:0;padding:2rem}
395
+ .card{max-width:640px;margin:auto;background:#0d1016;border:1px solid #1d2430;border-radius:12px;padding:1.5rem}
396
+ h1{font-size:1.1rem;color:#7fd1ff}pre{white-space:pre-wrap;background:#06080c;border:1px solid #1d2430;border-radius:8px;padding:.75rem;font-size:12px}
397
+ button{font:inherit;background:#153;border:1px solid #2a6;color:#cfe;border-radius:8px;padding:.5rem 1rem;cursor:pointer}
398
+ .ok{color:#5de28a}.bad{color:#ff6b6b}.warn{color:#e2b85d}.muted{color:#7a828e}</style></head><body><div class="card">
399
+ <h1>✦ dexterm · link wallet</h1>
400
+ <p>Your <b>dexterm</b> terminal opened this page on <code>127.0.0.1</code>. It asks your wallet to <b>sign a message</b> to prove ownership — no transaction, no funds, no approvals.</p>
401
+ <pre id="c">${esc(challenge)}</pre>${portNote ? `\n<p class="warn">${esc(portNote)}</p>` : ''}
402
+ <p id="p" class="warn">looking for a wallet…</p>
403
+ <p><button id="go" disabled>Connect &amp; sign</button> <span id="s"></span></p>
404
+ <p id="r"></p><p class="muted">Close this tab once the terminal shows <b>linked</b>.</p></div>
405
+ <script nonce="${nonce}">(()=>{'use strict';const $=i=>document.getElementById(i),PORT=${port},challenge=$('c').textContent;
406
+ const b64=u=>btoa(String.fromCharCode.apply(null,u));let prov=null,name='none';
407
+ function find(){if(window.phantom&&window.phantom.solana&&window.phantom.solana.isPhantom)return[window.phantom.solana,'phantom'];
408
+ if(window.solana&&window.solana.isPhantom)return[window.solana,'phantom'];if(window.solana&&window.solana.signMessage)return[window.solana,window.solana.isStubWallet?'stub':'solana'];return[null,'none'];}
409
+ function poll(n){[prov,name]=find();if(prov){$('p').textContent='wallet: '+name;$('p').className='ok';$('go').disabled=false;if(name==='stub')run();return;}
410
+ if(n>40){$('p').textContent='No Solana wallet found. Install Phantom or open this page in the browser that has it.';$('p').className='bad';return;}setTimeout(()=>poll(n+1),100);}
411
+ async function run(){$('go').disabled=true;$('s').textContent='connecting…';try{const res=await prov.connect();const pk=(res&&res.publicKey)||prov.publicKey;
412
+ const pkb=pk&&pk.toBytes?pk.toBytes():null;if(!pkb||pkb.length!==32)throw new Error('no 32-byte public key');
413
+ $('s').textContent='sign the message in your wallet';const sg=await prov.signMessage(new TextEncoder().encode(challenge),'utf8');const sig=(sg&&sg.signature)||sg;
414
+ if(!(sig instanceof Uint8Array)||sig.length!==64)throw new Error('no 64-byte signature');
415
+ const rp=await fetch('http://127.0.0.1:'+PORT+'/link/result',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({t:${JSON.stringify('__T__')},publicKey:b64(pkb),signature:b64(sig),challenge:challenge,provider:name})});
416
+ const j=await rp.json();if(j.ok){$('r').textContent='✓ linked. Return to dexterm.';$('r').className='ok';$('s').textContent='done';}else{$('r').textContent='✗ '+(j.error||'rejected');$('r').className='bad';$('go').disabled=false;}}
417
+ catch(e){$('r').textContent='✗ '+(e&&e.message||e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
418
+ $('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
419
+ }
420
+ function signPage(port, nonce, unsignedTxB64, expectPk, label, portNote = '') {
421
+ const web3 = solanaWeb3Bundle();
422
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1"><title>dexterm · sign</title>
424
+ <style nonce="${nonce}">body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#07090d;color:#d7dbe0;margin:0;padding:2rem}
425
+ .card{max-width:640px;margin:auto;background:#0d1016;border:1px solid #1d2430;border-radius:12px;padding:1.5rem}
426
+ h1{font-size:1.1rem;color:#7fd1ff}button{font:inherit;background:#531;border:1px solid #a62;color:#fec;border-radius:8px;padding:.5rem 1rem;cursor:pointer}
427
+ .ok{color:#5de28a}.bad{color:#ff6b6b}.warn{color:#e2b85d}.muted{color:#7a828e}code{word-break:break-all}</style></head><body><div class="card">
428
+ <h1>✦ dexterm · approve ${esc(label)}</h1>
429
+ <p>Your terminal already showed you this trade and you pressed confirm. Approve it in your wallet to sign. dexterm submits the signed transaction; it never holds your key.</p>
430
+ ${portNote ? `<p class="warn">${esc(portNote)}</p>\n` : ''}<p id="p" class="warn">looking for a wallet…</p>
431
+ <p><button id="go" disabled>Approve in wallet</button> <span id="s"></span></p>
432
+ <p id="r"></p>
433
+ <p class="muted">linked wallet: <code>${esc(expectPk || 'any')}</code> · connected: <code id="a">–</code></p></div>
434
+ ${web3 ? `<script nonce="${nonce}">${web3}\n;window.dexSolana=solanaWeb3;</script>` : ''}
435
+ <script nonce="${nonce}">(()=>{'use strict';const $=i=>document.getElementById(i),PORT=${port},EXPECT=${JSON.stringify(expectPk)};
436
+ const TX=${JSON.stringify(unsignedTxB64)};const b64=u=>btoa(String.fromCharCode.apply(null,u));
437
+ const short=k=>k&&k.length>12?k.slice(0,4)+'…'+k.slice(-4):String(k||'?');
438
+ // task-1368 (g): Phantom's error codes in plain English. 4100 = this origin was never connected (or the
439
+ // user revoked it): the fix is the Connect prompt, not a retry of the signature.
440
+ function explain(e){const code=e&&(e.code!==undefined?e.code:(e.error&&e.error.code));const msg=(e&&e.message)||String(e);
441
+ if(code===4100)return 'Phantom has not authorized this page (4100). Press Approve again and accept the Connect prompt; if none appears, open Phantom → Settings → Trusted apps, allow 127.0.0.1:'+PORT+', then confirm the trade again in dexterm. Nothing was signed.';
442
+ if(code===4001)return 'you rejected the request in Phantom; nothing was signed. Confirm again in dexterm to retry.';
443
+ if(code===-32603)return 'Phantom could not process the transaction (internal error): '+msg;
444
+ return msg;}
445
+ function bytes(b){const s=atob(b),a=new Uint8Array(s.length);for(let i=0;i<s.length;i++)a[i]=s.charCodeAt(i);return a;}
446
+ let prov=null,name='none';function find(){if(window.phantom&&window.phantom.solana&&window.phantom.solana.isPhantom)return[window.phantom.solana,'phantom'];if(window.solana&&window.solana.isPhantom)return[window.solana,'phantom'];if(window.solana)return[window.solana,window.solana.isStubWallet?'stub':'solana'];return[null,'none'];}
447
+ function poll(n){[prov,name]=find();if(prov){$('p').textContent='wallet: '+name;$('p').className='ok';$('go').disabled=false;if(name==='stub')run();return;}if(n>40){$('p').textContent='No Solana wallet found.';$('p').className='bad';return;}setTimeout(()=>poll(n+1),100);}
448
+ async function run(){$('go').disabled=true;$('s').textContent='connecting to your wallet…';try{
449
+ // Real Phantom needs @solana/web3.js on the page to rebuild the VersionedTransaction before signTransaction().
450
+ // The dexterm wallet shell (popup-host lane) provides it as window.dexSolana; see scripts/ for the manual path.
451
+ if(!window.dexSolana||!window.dexSolana.VersionedTransaction)throw new Error('tx signing needs the dexterm wallet shell (window.dexSolana). See scripts/phantom-devnet.md.');
452
+ // task-1368 (g): CONNECT FIRST. signTransaction() on an origin Phantom has not connected is a 4100, so
453
+ // the page used to fail on every fresh (random-port) origin. connect() resolves silently on a trusted
454
+ // origin and prompts once on a new one; then the connected account must be the wallet dexterm linked.
455
+ const res=await prov.connect();const pk=(res&&res.publicKey)||prov.publicKey;const account=pk?(typeof pk.toBase58==='function'?pk.toBase58():String(pk)):'';
456
+ if(!account)throw new Error('the wallet connected without a public key; nothing was signed');$('a').textContent=account;
457
+ if(EXPECT&&account!==EXPECT)throw new Error('Phantom is on account '+short(account)+' but dexterm linked '+short(EXPECT)+'. Switch accounts in Phantom and press Approve again, or re-link with "dexterm wallet link". Nothing was signed.');
458
+ $('s').textContent='approve in your wallet…';
459
+ const vt=window.dexSolana.VersionedTransaction.deserialize(bytes(TX));const signed=await prov.signTransaction(vt);
460
+ const out=b64(signed.serialize());const rp=await fetch('http://127.0.0.1:'+PORT+'/sign/result',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({t:${JSON.stringify('__T__')},signedTx:out,provider:name,publicKey:account})});
461
+ const j=await rp.json();if(j.ok){$('r').textContent='✓ signed. Return to dexterm.';$('r').className='ok';$('s').textContent='done';}else{$('r').textContent='✗ '+(j.error||'rejected');$('r').className='bad';$('go').disabled=false;}}
462
+ catch(e){$('r').textContent='✗ '+explain(e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
463
+ $('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
464
+ }