gloomberb 0.10.1 → 0.10.2

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 (351) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
+ "packageManager": "bun@1.3.11",
4
5
  "description": "Market research and portfolio tracker for the terminal",
5
6
  "module": "src/index.tsx",
6
7
  "type": "module",
@@ -23,6 +24,11 @@
23
24
  "desktop:release": "electrobun build --env=stable",
24
25
  "desktop:release:windows": "electrobun build --env=stable --targets=win-x64",
25
26
  "i18n:audit": "bun run scripts/i18n-audit.ts",
27
+ "typecheck": "bun run typecheck:opentui && bun run typecheck:electrobun-bun && bun run typecheck:electrobun-view && bun run typecheck:scripts",
28
+ "typecheck:opentui": "tsc --project tsconfig.opentui.json",
29
+ "typecheck:electrobun-bun": "tsc --project tsconfig.electrobun-bun.json",
30
+ "typecheck:electrobun-view": "tsc --project tsconfig.electrobun-view.json",
31
+ "typecheck:scripts": "tsc --project tsconfig.scripts.json",
26
32
  "test": "bun test",
27
33
  "release": "./scripts/release.sh"
28
34
  },
@@ -41,6 +47,7 @@
41
47
  "devDependencies": {
42
48
  "@types/bun": "^1.3.14",
43
49
  "@types/react-dom": "^19.2.3",
50
+ "@types/three": "0.165.0",
44
51
  "electrobun": "^1.18.1",
45
52
  "png-to-ico": "^2.1.8",
46
53
  "rcedit": "^4.0.1"
@@ -38,6 +38,7 @@ export class CloudAuthApi {
38
38
  username: typeof user.username === "string" ? user.username : null,
39
39
  emailVerified: user.emailVerified === true,
40
40
  image: typeof user.image === "string" ? user.image : null,
41
+ plan: user.plan,
41
42
  syncEnabled: user.syncEnabled === false ? false : true,
42
43
  weeklyRoundupEnabled: user.weeklyRoundupEnabled === false ? false : true,
43
44
  positionAlertsEnabled: user.positionAlertsEnabled === false ? false : true,
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, describe, expect, jest, test } from "bun:test";
2
2
  import type { AuthUser } from "./index";
3
3
  import { apiClient, setCloudApiFetchTransport } from "./index";
4
4
 
@@ -37,6 +37,66 @@ function mockFetch(handler: (input: Request | string | URL, init?: RequestInit)
37
37
  return handler as unknown as typeof fetch;
38
38
  }
39
39
 
40
+ class TestWebSocket {
41
+ readyState: number;
42
+ onopen: ((event: unknown) => void) | null = null;
43
+ onmessage: ((event: { data: string }) => void) | null = null;
44
+ onclose: ((event: unknown) => void) | null = null;
45
+ onerror: ((event: unknown) => void) | null = null;
46
+ readonly sent: unknown[] = [];
47
+ closeCalls = 0;
48
+
49
+ constructor(
50
+ readonly url: string,
51
+ initialReadyState: number,
52
+ ) {
53
+ this.readyState = initialReadyState;
54
+ }
55
+
56
+ send(payload: string): void {
57
+ this.sent.push(JSON.parse(payload));
58
+ }
59
+
60
+ close(): void {
61
+ this.closeCalls += 1;
62
+ this.readyState = 3;
63
+ }
64
+
65
+ open(): void {
66
+ this.readyState = 1;
67
+ this.onopen?.({});
68
+ }
69
+
70
+ receive(payload: unknown): void {
71
+ this.onmessage?.({ data: JSON.stringify(payload) });
72
+ }
73
+
74
+ closeWith(event: { code: number; reason: string }): void {
75
+ this.readyState = 3;
76
+ this.onclose?.(event);
77
+ }
78
+ }
79
+
80
+ function installTestWebSocket(initialReadyState = 1): TestWebSocket[] {
81
+ const sockets: TestWebSocket[] = [];
82
+
83
+ class InstalledTestWebSocket extends TestWebSocket {
84
+ static readonly OPEN = 1;
85
+
86
+ constructor(url: string) {
87
+ super(url, initialReadyState);
88
+ sockets.push(this);
89
+ }
90
+ }
91
+
92
+ globalThis.WebSocket = InstalledTestWebSocket as unknown as typeof WebSocket;
93
+ return sockets;
94
+ }
95
+
96
+ function flushQuoteSubscriptionUpdates(): void {
97
+ jest.runAllTimers();
98
+ }
99
+
40
100
  afterEach(() => {
41
101
  apiClient.dispose();
42
102
  globalThis.fetch = originalFetch;
@@ -44,6 +104,7 @@ afterEach(() => {
44
104
  setCloudApiFetchTransport(null);
45
105
  apiClient.setSessionToken(null);
46
106
  apiClient.setWebSocketToken(null);
107
+ jest.useRealTimers();
47
108
  });
48
109
 
49
110
  describe("apiClient auth cookies", () => {
@@ -164,36 +225,11 @@ describe("apiClient auth cookies", () => {
164
225
  expect(apiClient.getWebSocketToken()).toBeNull();
165
226
  expect(apiClient.getCurrentUser()).toBeNull();
166
227
  });
228
+ });
167
229
 
230
+ describe("apiClient quote socket", () => {
168
231
  test("drops a stale websocket token after socket close so reconnect can use the session token", () => {
169
- const sockets: Array<{
170
- url: string;
171
- readyState: number;
172
- onclose: ((event: unknown) => void) | null;
173
- close: () => void;
174
- }> = [];
175
-
176
- class FakeWebSocket {
177
- static OPEN = 1;
178
- readyState = 0;
179
- onopen: ((event: unknown) => void) | null = null;
180
- onmessage: ((event: { data: string }) => void) | null = null;
181
- onclose: ((event: unknown) => void) | null = null;
182
- onerror: ((event: unknown) => void) | null = null;
183
-
184
- constructor(readonly url: string) {
185
- sockets.push(this);
186
- }
187
-
188
- send(): void {}
189
-
190
- close(): void {
191
- this.readyState = 3;
192
- this.onclose?.({ code: 1000, reason: "closed" });
193
- }
194
- }
195
-
196
- globalThis.WebSocket = FakeWebSocket as any;
232
+ const sockets = installTestWebSocket(0);
197
233
  apiClient.setSessionToken("session-token");
198
234
  apiClient.setWebSocketToken("stale-ws-token");
199
235
  apiClient.restoreCachedUser(verifiedUser);
@@ -203,7 +239,7 @@ describe("apiClient auth cookies", () => {
203
239
  expect(sockets).toHaveLength(1);
204
240
  expect(sockets[0]!.url).toContain("token=stale-ws-token");
205
241
 
206
- sockets[0]!.onclose?.({ code: 1008, reason: "Unauthorized" });
242
+ sockets[0]!.closeWith({ code: 1008, reason: "Unauthorized" });
207
243
 
208
244
  expect(apiClient.getWebSocketToken()).toBeNull();
209
245
  expect(apiClient.getSessionToken()).toBe("session-token");
@@ -212,36 +248,7 @@ describe("apiClient auth cookies", () => {
212
248
  });
213
249
 
214
250
  test("opens an anonymous market websocket and sends quote priority hints", () => {
215
- const sent: unknown[] = [];
216
- const sockets: Array<{
217
- url: string;
218
- readyState: number;
219
- onopen: ((event: unknown) => void) | null;
220
- close: () => void;
221
- }> = [];
222
-
223
- class FakeWebSocket {
224
- static OPEN = 1;
225
- readyState = 1;
226
- onopen: ((event: unknown) => void) | null = null;
227
- onmessage: ((event: { data: string }) => void) | null = null;
228
- onclose: ((event: unknown) => void) | null = null;
229
- onerror: ((event: unknown) => void) | null = null;
230
-
231
- constructor(readonly url: string) {
232
- sockets.push(this);
233
- }
234
-
235
- send(payload: string): void {
236
- sent.push(JSON.parse(payload));
237
- }
238
-
239
- close(): void {
240
- this.readyState = 3;
241
- }
242
- }
243
-
244
- globalThis.WebSocket = FakeWebSocket as any;
251
+ const sockets = installTestWebSocket();
245
252
 
246
253
  const unsubscribe = apiClient.subscribeQuotes([{
247
254
  symbol: "AAPL",
@@ -251,11 +258,11 @@ describe("apiClient auth cookies", () => {
251
258
  selected: true,
252
259
  weight: 100,
253
260
  }], () => {});
254
- sockets[0]!.onopen?.({});
261
+ sockets[0]!.open();
255
262
 
256
263
  expect(sockets).toHaveLength(1);
257
264
  expect(sockets[0]!.url).toBe("wss://api.gloom.sh/cloud/ws");
258
- expect(sent).toContainEqual({
265
+ expect(sockets[0]!.sent).toContainEqual({
259
266
  type: "market.subscribe",
260
267
  symbols: [{
261
268
  symbol: "AAPL",
@@ -270,62 +277,306 @@ describe("apiClient auth cookies", () => {
270
277
  unsubscribe();
271
278
  });
272
279
 
273
- test("keeps an anonymous market websocket open after auth rejection", () => {
274
- const seenPrices: number[] = [];
275
- let closeCalls = 0;
276
- const sockets: Array<{
277
- readyState: number;
278
- onopen: ((event: unknown) => void) | null;
279
- onmessage: ((event: { data: string }) => void) | null;
280
- close: () => void;
281
- }> = [];
282
-
283
- class FakeWebSocket {
284
- static OPEN = 1;
285
- readyState = 1;
286
- onopen: ((event: unknown) => void) | null = null;
287
- onmessage: ((event: { data: string }) => void) | null = null;
288
- onclose: ((event: unknown) => void) | null = null;
289
- onerror: ((event: unknown) => void) | null = null;
290
-
291
- constructor(readonly _url: string) {
292
- sockets.push(this);
293
- }
280
+ test("reconnects and replays quote targets when market entitlement changes", () => {
281
+ const sockets = installTestWebSocket();
282
+ apiClient.setSessionToken("session-token");
283
+ apiClient.restoreCachedUser({ ...verifiedUser, plan: "free" });
284
+ const target = {
285
+ symbol: "AAPL260731C00110000",
286
+ exchange: "OPTIONS",
287
+ surface: "options" as const,
288
+ visible: true,
289
+ };
290
+ const unsubscribe = apiClient.subscribeQuotes([target], () => {});
291
+ sockets[0]!.open();
294
292
 
295
- send(): void {}
293
+ apiClient.restoreCachedUser({ ...verifiedUser, plan: "pro" });
296
294
 
297
- close(): void {
298
- closeCalls += 1;
299
- this.readyState = 3;
300
- }
301
- }
295
+ expect(sockets).toHaveLength(2);
296
+ expect(sockets[0]!.closeCalls).toBe(1);
297
+ expect(sockets[1]!.url).toContain("token=session-token");
298
+ sockets[1]!.open();
299
+ expect(sockets[1]!.sent).toContainEqual({
300
+ type: "market.subscribe",
301
+ symbols: [target],
302
+ });
303
+ unsubscribe();
304
+ });
305
+
306
+ test("serializes and dispatches compact OCC option quote targets", () => {
307
+ const sockets = installTestWebSocket();
308
+ const seen: Array<{ symbol: string; dataSource: string | undefined }> = [];
309
+ const unsubscribe = apiClient.subscribeQuotes(
310
+ [
311
+ {
312
+ symbol: "AAPL260731C00110000",
313
+ exchange: "OPTIONS",
314
+ surface: "options",
315
+ visible: true,
316
+ selected: true,
317
+ weight: 100,
318
+ },
319
+ ],
320
+ (target, quote) => {
321
+ seen.push({ symbol: target.symbol, dataSource: quote.dataSource });
322
+ },
323
+ );
324
+ const socket = sockets[0]!;
325
+ socket.open();
326
+
327
+ expect(socket.sent).toContainEqual({
328
+ type: "market.subscribe",
329
+ symbols: [
330
+ {
331
+ symbol: "AAPL260731C00110000",
332
+ exchange: "OPTIONS",
333
+ surface: "options",
334
+ visible: true,
335
+ selected: true,
336
+ weight: 100,
337
+ },
338
+ ],
339
+ });
340
+
341
+ socket.receive({
342
+ type: "market.quote",
343
+ symbol: "AAPL260731C00110000",
344
+ exchange: "OPTIONS",
345
+ quote: {
346
+ symbol: "AAPL260731C00110000",
347
+ providerId: "gloomberb-cloud",
348
+ price: 2.5,
349
+ currency: "USD",
350
+ change: 0,
351
+ changePercent: 0,
352
+ lastUpdated: 1_800_000_000_000,
353
+ dataSource: "live",
354
+ },
355
+ });
356
+
357
+ expect(seen).toEqual([
358
+ {
359
+ symbol: "AAPL260731C00110000",
360
+ dataSource: "live",
361
+ },
362
+ ]);
363
+ unsubscribe();
364
+ });
365
+
366
+ test("preserves quote delivery and stale metadata from websocket messages", () => {
367
+ const sockets = installTestWebSocket();
368
+ const seen: Array<{ delivery?: string; stale?: boolean }> = [];
369
+ const unsubscribe = apiClient.subscribeQuotes(
370
+ [{ symbol: "AAPL260731C00110000", exchange: "OPTIONS" }],
371
+ (_target, quote) => {
372
+ seen.push({ delivery: quote.delivery, stale: quote.stale });
373
+ },
374
+ );
375
+ const socket = sockets[0]!;
376
+ socket.open();
377
+
378
+ socket.receive({
379
+ type: "market.quote",
380
+ symbol: "AAPL260731C00110000",
381
+ exchange: "OPTIONS",
382
+ delivery: "poll",
383
+ stale: true,
384
+ quote: {
385
+ symbol: "AAPL260731C00110000",
386
+ providerId: "gloomberb-cloud",
387
+ price: 2.5,
388
+ currency: "USD",
389
+ change: 0,
390
+ changePercent: 0,
391
+ lastUpdated: 1_800_000_000_000,
392
+ dataSource: "live",
393
+ },
394
+ });
395
+
396
+ expect(seen).toEqual([{ delivery: "poll", stale: true }]);
397
+ unsubscribe();
398
+ });
399
+
400
+ test("does not invent quote priority hints when opening a socket", () => {
401
+ const sockets = installTestWebSocket();
402
+
403
+ const unsubscribe = apiClient.subscribeQuotes([{ symbol: "AAPL", exchange: "NASDAQ" }], () => {});
404
+ sockets[0]!.open();
405
+
406
+ expect(sockets[0]!.sent).toContainEqual({
407
+ type: "market.subscribe",
408
+ symbols: [{ symbol: "AAPL", exchange: "NASDAQ" }],
409
+ });
302
410
 
303
- globalThis.WebSocket = FakeWebSocket as any;
411
+ unsubscribe();
412
+ });
413
+
414
+ test("recomputes quote priority when overlapping subscriptions are removed", () => {
415
+ jest.useFakeTimers();
416
+ const sockets = installTestWebSocket();
417
+ const deliveredSurfaces: string[] = [];
418
+ const unsubscribeInline = apiClient.subscribeQuotes([{
419
+ symbol: "AAPL",
420
+ exchange: "NASDAQ",
421
+ surface: "inline",
422
+ weight: 1,
423
+ }], (target) => {
424
+ deliveredSurfaces.push(target.surface ?? "unknown");
425
+ });
426
+ const socket = sockets[0]!;
427
+ socket.open();
428
+ flushQuoteSubscriptionUpdates();
429
+ socket.sent.length = 0;
430
+
431
+ const unsubscribeDetail = apiClient.subscribeQuotes([{
432
+ symbol: "AAPL",
433
+ exchange: "NASDAQ",
434
+ surface: "detail",
435
+ visible: true,
436
+ selected: true,
437
+ weight: 50,
438
+ }], (target) => {
439
+ deliveredSurfaces.push(target.surface ?? "unknown");
440
+ });
441
+ flushQuoteSubscriptionUpdates();
442
+
443
+ expect(socket.sent.at(-1)).toEqual({
444
+ type: "market.subscribe",
445
+ symbols: [{
446
+ symbol: "AAPL",
447
+ exchange: "NASDAQ",
448
+ surface: "detail",
449
+ visible: true,
450
+ selected: true,
451
+ weight: 50,
452
+ }],
453
+ });
454
+
455
+ socket.receive({
456
+ type: "market.quote",
457
+ symbol: "AAPL",
458
+ exchange: "NASDAQ",
459
+ quote: { symbol: "AAPL", price: 123 },
460
+ });
461
+ expect(deliveredSurfaces).toEqual(["inline", "detail"]);
462
+
463
+ unsubscribeDetail();
464
+ flushQuoteSubscriptionUpdates();
465
+ expect(socket.sent.at(-1)).toEqual({
466
+ type: "market.subscribe",
467
+ symbols: [{
468
+ symbol: "AAPL",
469
+ exchange: "NASDAQ",
470
+ surface: "inline",
471
+ weight: 1,
472
+ }],
473
+ });
474
+
475
+ unsubscribeInline();
476
+ });
477
+
478
+ test("unsubscribes a server-side quote when queued priority updates are removed", () => {
479
+ jest.useFakeTimers();
480
+ const sockets = installTestWebSocket();
481
+ const unsubscribeMsft = apiClient.subscribeQuotes([{ symbol: "MSFT", exchange: "NASDAQ" }], () => {});
482
+ const socket = sockets[0]!;
483
+ socket.open();
484
+ flushQuoteSubscriptionUpdates();
485
+
486
+ const unsubscribeInline = apiClient.subscribeQuotes([{
487
+ symbol: "AAPL",
488
+ exchange: "NASDAQ",
489
+ surface: "inline",
490
+ weight: 1,
491
+ }], () => {});
492
+ flushQuoteSubscriptionUpdates();
493
+ expect(socket.sent.at(-1)).toEqual({
494
+ type: "market.subscribe",
495
+ symbols: [{ symbol: "AAPL", exchange: "NASDAQ", surface: "inline", weight: 1 }],
496
+ });
497
+
498
+ socket.sent.length = 0;
499
+ const unsubscribeDetail = apiClient.subscribeQuotes([{
500
+ symbol: "AAPL",
501
+ exchange: "NASDAQ",
502
+ surface: "detail",
503
+ visible: true,
504
+ selected: true,
505
+ weight: 50,
506
+ }], () => {});
507
+ unsubscribeDetail();
508
+ unsubscribeInline();
509
+ flushQuoteSubscriptionUpdates();
510
+
511
+ expect(socket.sent).toEqual([{
512
+ type: "market.unsubscribe",
513
+ symbols: [{ symbol: "AAPL", exchange: "NASDAQ", surface: "inline", weight: 1 }],
514
+ }]);
515
+ unsubscribeMsft();
516
+ });
517
+
518
+ test("unsubscribes removed quote targets before subscribing replacements", () => {
519
+ jest.useFakeTimers();
520
+ const sockets = installTestWebSocket();
521
+ const oldTargets = Array.from({ length: 16 }, (_, index) => ({
522
+ symbol: `OPT${index}`,
523
+ exchange: "OPTIONS",
524
+ }));
525
+ const newTargets = Array.from({ length: 16 }, (_, index) => ({
526
+ symbol: `OPT${index + 2}`,
527
+ exchange: "OPTIONS",
528
+ }));
529
+ const unsubscribeOld = apiClient.subscribeQuotes(oldTargets, () => {});
530
+ const socket = sockets[0]!;
531
+ socket.open();
532
+ flushQuoteSubscriptionUpdates();
533
+ socket.sent.length = 0;
534
+
535
+ const unsubscribeNew = apiClient.subscribeQuotes(newTargets, () => {});
536
+ unsubscribeOld();
537
+ flushQuoteSubscriptionUpdates();
538
+
539
+ expect(socket.sent.map((message) => (message as { type: string }).type)).toEqual([
540
+ "market.unsubscribe",
541
+ "market.subscribe",
542
+ ]);
543
+ expect(socket.sent[0]).toMatchObject({
544
+ symbols: [{ symbol: "OPT0" }, { symbol: "OPT1" }],
545
+ });
546
+ expect(socket.sent[1]).toMatchObject({
547
+ symbols: [{ symbol: "OPT16" }, { symbol: "OPT17" }],
548
+ });
549
+
550
+ unsubscribeNew();
551
+ });
552
+
553
+ test("keeps an anonymous market websocket open after auth rejection", () => {
554
+ const seenPrices: number[] = [];
555
+ const sockets = installTestWebSocket();
304
556
 
305
557
  const unsubscribe = apiClient.subscribeQuotes([{ symbol: "AAPL" }], (_target, quote) => {
306
558
  seenPrices.push(quote.price);
307
559
  });
308
- sockets[0]!.onopen?.({});
309
- sockets[0]!.onmessage?.({ data: JSON.stringify({ type: "auth.unverified" }) });
310
- sockets[0]!.onmessage?.({
311
- data: JSON.stringify({
312
- type: "market.quote",
560
+ const socket = sockets[0]!;
561
+ socket.open();
562
+ socket.receive({ type: "auth.unverified" });
563
+ socket.receive({
564
+ type: "market.quote",
565
+ symbol: "AAPL",
566
+ exchange: "",
567
+ quote: {
313
568
  symbol: "AAPL",
314
- exchange: "",
315
- quote: {
316
- symbol: "AAPL",
317
- price: 123,
318
- currency: "USD",
319
- change: 0,
320
- changePercent: 0,
321
- lastUpdated: 1,
322
- providerId: "gloomberb-cloud",
323
- dataSource: "live",
324
- },
325
- }),
569
+ price: 123,
570
+ currency: "USD",
571
+ change: 0,
572
+ changePercent: 0,
573
+ lastUpdated: 1,
574
+ providerId: "gloomberb-cloud",
575
+ dataSource: "live",
576
+ },
326
577
  });
327
578
 
328
- expect(closeCalls).toBe(0);
579
+ expect(socket.closeCalls).toBe(0);
329
580
  expect(seenPrices).toEqual([123]);
330
581
 
331
582
  unsubscribe();
@@ -70,11 +70,7 @@ class GloomApiClient {
70
70
  requireCapturedSession: (message) => this.requireCapturedSession(message),
71
71
  setCurrentUser: (user) => this.setCurrentUser(user),
72
72
  setSessionToken: (token) => this.setSessionToken(token),
73
- updateCurrentUser: (updater) => {
74
- if (this.currentUser) {
75
- this.currentUser = updater(this.currentUser);
76
- }
77
- },
73
+ updateCurrentUser: (updater) => this.updateCurrentUser(updater),
78
74
  });
79
75
  this.socket = new CloudApiSocket({
80
76
  getBaseUrl: () => this.transport.baseUrl,
@@ -88,10 +84,10 @@ class GloomApiClient {
88
84
  }
89
85
  },
90
86
  updateCurrentUserFromSocket: (user) => {
91
- this.currentUser = {
92
- ...(this.currentUser ?? {}),
87
+ this.updateCurrentUser((currentUser) => ({
88
+ ...currentUser,
93
89
  ...user,
94
- } as AuthUser;
90
+ }));
95
91
  },
96
92
  });
97
93
  this.chat = new CloudChatApi({
@@ -110,18 +106,18 @@ class GloomApiClient {
110
106
  }
111
107
 
112
108
  setSessionToken(token: string | null): void {
109
+ const changed = this.transport.getSessionToken() !== token;
113
110
  this.transport.setSessionToken(token);
114
111
  if (!token) {
115
112
  this.currentUser = null;
116
- this.socket.teardown();
117
113
  }
114
+ this.socket.syncAuthState({ reconnect: changed });
118
115
  }
119
116
 
120
117
  setWebSocketToken(token: string | null): void {
118
+ const changed = this.transport.getWebSocketToken() !== token;
121
119
  this.transport.setWebSocketToken(token);
122
- if (!token) {
123
- this.socket.teardown();
124
- }
120
+ this.socket.syncAuthState({ reconnect: changed });
125
121
  }
126
122
 
127
123
  getCurrentUser(): AuthUser | null {
@@ -137,8 +133,23 @@ class GloomApiClient {
137
133
  }
138
134
 
139
135
  private setCurrentUser(user: AuthUser | null): void {
136
+ const changed = this.socketEntitlementKey(this.currentUser) !== this.socketEntitlementKey(user);
140
137
  this.currentUser = user;
141
- this.socket.syncAuthState();
138
+ this.socket.syncAuthState({ reconnect: changed });
139
+ }
140
+
141
+ private updateCurrentUser(updater: (user: AuthUser) => AuthUser): void {
142
+ if (!this.currentUser) return;
143
+ this.setCurrentUser(updater(this.currentUser));
144
+ }
145
+
146
+ private socketEntitlementKey(user: AuthUser | null): string {
147
+ if (!user) return "anonymous";
148
+ return [
149
+ user.id,
150
+ user.emailVerified === true ? "verified" : "unverified",
151
+ user.plan,
152
+ ].join(":");
142
153
  }
143
154
 
144
155
  private requireCapturedSession(message: string): void {