gloomberb 0.11.0 → 0.11.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.
- package/README.md +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { apiClient } from "../api-client";
|
|
2
|
+
import { ApiRequestError } from "../api-client/errors";
|
|
3
|
+
import { parseSharePayload, type SharePayload } from "./payload";
|
|
4
|
+
|
|
5
|
+
declare const __GLOOMBERB_API_URL__: string | undefined;
|
|
6
|
+
|
|
7
|
+
const bundledApiOrigin = typeof __GLOOMBERB_API_URL__ === "string" ? __GLOOMBERB_API_URL__ : "";
|
|
8
|
+
export const SHARE_API_ORIGIN = bundledApiOrigin
|
|
9
|
+
? typeof location !== "undefined" && bundledApiOrigin === location.origin
|
|
10
|
+
? `${bundledApiOrigin}/api`
|
|
11
|
+
: bundledApiOrigin
|
|
12
|
+
: "https://api.gloom.sh";
|
|
13
|
+
export const PUBLIC_SHARE_ORIGIN = "https://term.gloom.sh";
|
|
14
|
+
const SHARE_ID = /^[a-f0-9]{32}$/;
|
|
15
|
+
type ShareFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
16
|
+
|
|
17
|
+
export type ShareRecord = SharePayload & {
|
|
18
|
+
createdAt: string;
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
ownedByViewer: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface CreatedShare {
|
|
24
|
+
id: string;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validDate(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readJson(response: Response): Promise<unknown> {
|
|
33
|
+
try { return await response.json(); } catch { throw new Error("The share service returned an invalid response."); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createShare(payload: SharePayload, fetchImpl?: ShareFetch): Promise<CreatedShare> {
|
|
37
|
+
const validated = parseSharePayload(payload);
|
|
38
|
+
if (!validated) throw new Error("Invalid share payload.");
|
|
39
|
+
let body: unknown;
|
|
40
|
+
if (fetchImpl) {
|
|
41
|
+
const response = await fetchImpl(`${SHARE_API_ORIGIN}/shares`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
credentials: "include",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify(validated),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
if (response.status === 401) throw new Error("Sign in to Gloom Cloud to share.");
|
|
49
|
+
if (response.status === 403) throw new Error("Verify your Gloom Cloud email to share.");
|
|
50
|
+
throw new Error("Could not create share.");
|
|
51
|
+
}
|
|
52
|
+
body = await readJson(response);
|
|
53
|
+
} else {
|
|
54
|
+
try {
|
|
55
|
+
body = await apiClient.createTerminalShare(validated);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof ApiRequestError && error.status === 401) throw new Error("Sign in to Gloom Cloud to share.");
|
|
58
|
+
if (error instanceof ApiRequestError && error.status === 403) throw new Error("Verify your Gloom Cloud email to share.");
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!body || typeof body !== "object") throw new Error("The share service returned an invalid response.");
|
|
63
|
+
const { id, expiresAt } = body as Record<string, unknown>;
|
|
64
|
+
if (typeof id !== "string" || !SHARE_ID.test(id) || !validDate(expiresAt)) {
|
|
65
|
+
throw new Error("The share service returned an invalid response.");
|
|
66
|
+
}
|
|
67
|
+
return { id, expiresAt };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getShare(
|
|
71
|
+
id: string,
|
|
72
|
+
fetchImpl: ShareFetch = fetch,
|
|
73
|
+
options?: { trackView?: boolean },
|
|
74
|
+
): Promise<ShareRecord | null> {
|
|
75
|
+
if (!SHARE_ID.test(id)) return null;
|
|
76
|
+
const purpose = options?.trackView === false ? "?purpose=open" : "";
|
|
77
|
+
const response = await fetchImpl(`${SHARE_API_ORIGIN}/shares/${encodeURIComponent(id)}${purpose}`, {
|
|
78
|
+
credentials: "include",
|
|
79
|
+
});
|
|
80
|
+
if (response.status === 404) return null;
|
|
81
|
+
if (!response.ok) throw new Error("Could not load share.");
|
|
82
|
+
const body = await readJson(response);
|
|
83
|
+
if (!body || typeof body !== "object") return null;
|
|
84
|
+
const object = body as Record<string, unknown>;
|
|
85
|
+
const payload = parseSharePayload({ kind: object.kind, data: object.data });
|
|
86
|
+
if (!payload || !validDate(object.createdAt) || !validDate(object.expiresAt)) return null;
|
|
87
|
+
return {
|
|
88
|
+
...payload,
|
|
89
|
+
createdAt: object.createdAt,
|
|
90
|
+
expiresAt: object.expiresAt,
|
|
91
|
+
ownedByViewer: object.ownedByViewer === true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function deleteShare(id: string, fetchImpl: ShareFetch = fetch): Promise<void> {
|
|
96
|
+
if (!SHARE_ID.test(id)) throw new Error("Invalid share id.");
|
|
97
|
+
const response = await fetchImpl(`${SHARE_API_ORIGIN}/shares/${encodeURIComponent(id)}`, {
|
|
98
|
+
method: "DELETE",
|
|
99
|
+
credentials: "include",
|
|
100
|
+
});
|
|
101
|
+
if (response.status !== 204) {
|
|
102
|
+
throw new Error(response.status === 401 || response.status === 403
|
|
103
|
+
? "Only the signed-in owner can delete this share."
|
|
104
|
+
: "Could not delete share.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function publicShareUrl(id: string, origin = PUBLIC_SHARE_ORIGIN): string {
|
|
109
|
+
if (!SHARE_ID.test(id)) throw new Error("Invalid share id.");
|
|
110
|
+
return new URL(`/s/${encodeURIComponent(id)}`, origin).toString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function openLiveShareUrl(id: string, origin = PUBLIC_SHARE_ORIGIN): string {
|
|
114
|
+
if (!SHARE_ID.test(id)) throw new Error("Invalid share id.");
|
|
115
|
+
return new URL(`/api/shares/${encodeURIComponent(id)}/open`, origin).toString();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseShareId(pathname: string): string | null {
|
|
119
|
+
const match = /^\/s\/([^/]+)\/?$/.exec(pathname);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
try {
|
|
122
|
+
const id = decodeURIComponent(match[1] ?? "");
|
|
123
|
+
return SHARE_ID.test(id) ? id : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ResolvedSeries } from "../time-series/types";
|
|
3
|
+
import { buildChartShareData } from "./chart-snapshot";
|
|
4
|
+
|
|
5
|
+
function series(pointCount: number): ResolvedSeries {
|
|
6
|
+
return {
|
|
7
|
+
id: "price",
|
|
8
|
+
label: "AAPL",
|
|
9
|
+
color: "#fff",
|
|
10
|
+
unit: "USD",
|
|
11
|
+
unitGroup: "currency:USD",
|
|
12
|
+
nativeFrequency: "daily",
|
|
13
|
+
dataShape: "scalar",
|
|
14
|
+
style: "line",
|
|
15
|
+
transform: "raw",
|
|
16
|
+
axis: "left",
|
|
17
|
+
panelId: "main",
|
|
18
|
+
interpolation: "none",
|
|
19
|
+
points: Array.from({ length: pointCount }, (_, index) => ({
|
|
20
|
+
date: new Date(Date.UTC(2025, 0, index + 1)),
|
|
21
|
+
observedAt: new Date(Date.UTC(2025, 0, index + 1)),
|
|
22
|
+
value: index,
|
|
23
|
+
})),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("chart share snapshots", () => {
|
|
28
|
+
test("keeps endpoints while bounding shared chart points", () => {
|
|
29
|
+
const shared = buildChartShareData([series(1_000)]);
|
|
30
|
+
expect(shared?.series[0]?.points).toHaveLength(500);
|
|
31
|
+
expect(shared?.series[0]?.points[0]?.y).toBe(0);
|
|
32
|
+
expect(shared?.series[0]?.points.at(-1)?.y).toBe(999);
|
|
33
|
+
});
|
|
34
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResolvedSeries } from "../time-series/types";
|
|
2
|
+
import type { ChartShareData } from "./payload";
|
|
3
|
+
|
|
4
|
+
const MAX_POINTS = 500;
|
|
5
|
+
|
|
6
|
+
function sample<T>(values: T[], limit: number): T[] {
|
|
7
|
+
if (values.length <= limit) return values;
|
|
8
|
+
return Array.from({ length: limit }, (_, index) => (
|
|
9
|
+
values[Math.round(index * (values.length - 1) / (limit - 1))]!
|
|
10
|
+
));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildChartShareData(series: ResolvedSeries[]): ChartShareData | null {
|
|
14
|
+
const sharedSeries = series.flatMap((entry) => {
|
|
15
|
+
const points = entry.points.flatMap((point) => {
|
|
16
|
+
const y = point.value ?? point.close;
|
|
17
|
+
return typeof y === "number" && Number.isFinite(y)
|
|
18
|
+
? [{ x: point.date.toISOString(), y }]
|
|
19
|
+
: [];
|
|
20
|
+
});
|
|
21
|
+
return points.length > 0 ? [{ name: entry.label, points: sample(points, MAX_POINTS) }] : [];
|
|
22
|
+
});
|
|
23
|
+
if (sharedSeries.length === 0) return null;
|
|
24
|
+
return {
|
|
25
|
+
title: sharedSeries.map((entry) => entry.name).join(" / "),
|
|
26
|
+
series: sharedSeries,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { isPaneShareHandoff, paneShareIdFromSearch } from "./location";
|
|
3
|
+
|
|
4
|
+
const originalWindow = (globalThis as { window?: unknown }).window;
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window;
|
|
8
|
+
else (globalThis as { window?: unknown }).window = originalWindow;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("pane share handoff tolerates renderer globals without browser location", () => {
|
|
12
|
+
(globalThis as { window?: unknown }).window = {};
|
|
13
|
+
expect(isPaneShareHandoff()).toBe(false);
|
|
14
|
+
|
|
15
|
+
const id = "0123456789abcdef0123456789abcdef";
|
|
16
|
+
(globalThis as { window?: unknown }).window = { location: { search: `?share=${id}` } };
|
|
17
|
+
expect(isPaneShareHandoff()).toBe(true);
|
|
18
|
+
expect(paneShareIdFromSearch(`?share=${id}`)).toBe(id);
|
|
19
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const SHARE_ID = /^[a-f0-9]{32}$/;
|
|
2
|
+
|
|
3
|
+
export function paneShareIdFromSearch(search: string): string | null {
|
|
4
|
+
const id = new URLSearchParams(search).get("share")?.trim() ?? "";
|
|
5
|
+
return SHARE_ID.test(id) ? id : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isPaneShareHandoff(): boolean {
|
|
9
|
+
const location = (globalThis as { window?: { location?: { search?: unknown } } }).window?.location;
|
|
10
|
+
return typeof location?.search === "string" && paneShareIdFromSearch(location.search) !== null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { LayoutMarketplacePayload } from "../layout-marketplace/payload";
|
|
3
|
+
import type { PaneDef, PaneTemplateDef } from "../types/plugin";
|
|
4
|
+
import { buildPaneSharePayload, openPaneShare } from "./pane";
|
|
5
|
+
|
|
6
|
+
const paneDef: PaneDef = {
|
|
7
|
+
id: "ticker-news",
|
|
8
|
+
name: "Ticker News",
|
|
9
|
+
component: () => null,
|
|
10
|
+
defaultPosition: "right",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function registry(template?: PaneTemplateDef, pane: PaneDef = paneDef) {
|
|
14
|
+
const opened: unknown[] = [];
|
|
15
|
+
const portable: LayoutMarketplacePayload[] = [];
|
|
16
|
+
return {
|
|
17
|
+
opened,
|
|
18
|
+
portable,
|
|
19
|
+
panes: new Map([[pane.id, pane]]),
|
|
20
|
+
paneTemplates: new Map(template ? [[template.id, template]] : []),
|
|
21
|
+
createPaneFromTemplateAsyncFn: async (templateId: string, options: unknown) => {
|
|
22
|
+
opened.push([templateId, options]);
|
|
23
|
+
},
|
|
24
|
+
openPortablePaneShareAsyncFn: async (layout: LayoutMarketplacePayload) => {
|
|
25
|
+
portable.push(layout);
|
|
26
|
+
},
|
|
27
|
+
} as any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("public pane shares", () => {
|
|
31
|
+
test("publishes registered panes through the generic portable projection", async () => {
|
|
32
|
+
const pluginRegistry = registry({
|
|
33
|
+
id: "ticker-news-pane",
|
|
34
|
+
paneId: "ticker-news",
|
|
35
|
+
label: "Ticker News",
|
|
36
|
+
description: "Current news for a ticker.",
|
|
37
|
+
});
|
|
38
|
+
const payload = buildPaneSharePayload(pluginRegistry, {
|
|
39
|
+
instanceId: "news:private-local-id",
|
|
40
|
+
paneId: "ticker-news",
|
|
41
|
+
title: "AAPL News",
|
|
42
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
43
|
+
params: { query: "earnings" },
|
|
44
|
+
settings: { dense: true, sessionToken: "secret" },
|
|
45
|
+
}, {
|
|
46
|
+
pluginState: { news: { selectedTab: "latest", accessToken: "secret" } },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(payload).toMatchObject({
|
|
50
|
+
kind: "pane",
|
|
51
|
+
data: {
|
|
52
|
+
version: 2,
|
|
53
|
+
title: "AAPL News",
|
|
54
|
+
description: "Current news for a ticker.",
|
|
55
|
+
layout: {
|
|
56
|
+
schemaVersion: 2,
|
|
57
|
+
layout: {
|
|
58
|
+
instances: [{
|
|
59
|
+
instanceId: "p1",
|
|
60
|
+
paneId: "ticker-news",
|
|
61
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
62
|
+
params: { query: "earnings" },
|
|
63
|
+
settings: { dense: true },
|
|
64
|
+
}],
|
|
65
|
+
},
|
|
66
|
+
paneState: {
|
|
67
|
+
p1: { pluginState: { news: { selectedTab: "latest" } } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(JSON.stringify(payload)).not.toContain("private-local-id");
|
|
73
|
+
expect(JSON.stringify(payload)).not.toContain("secret");
|
|
74
|
+
|
|
75
|
+
await openPaneShare(pluginRegistry, payload!.data);
|
|
76
|
+
expect(pluginRegistry.portable).toEqual([payload!.data.version === 2 ? payload!.data.layout : null]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("opens legacy adapter shares for existing links", async () => {
|
|
80
|
+
const template: PaneTemplateDef = {
|
|
81
|
+
id: "ticker-news-pane",
|
|
82
|
+
paneId: "ticker-news",
|
|
83
|
+
label: "Ticker News",
|
|
84
|
+
description: "Current news for a ticker.",
|
|
85
|
+
publicShare: {
|
|
86
|
+
serialize: () => null,
|
|
87
|
+
restore: (data) => typeof data.symbol === "string" ? { symbol: data.symbol } : null,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const pluginRegistry = registry(template);
|
|
91
|
+
|
|
92
|
+
await openPaneShare(pluginRegistry, {
|
|
93
|
+
version: 1,
|
|
94
|
+
templateId: template.id,
|
|
95
|
+
title: "AAPL News",
|
|
96
|
+
data: { symbol: "AAPL" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(pluginRegistry.opened).toEqual([["ticker-news-pane", { symbol: "AAPL" }]]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("does not share unregistered panes", () => {
|
|
103
|
+
const pluginRegistry = registry();
|
|
104
|
+
pluginRegistry.panes.clear();
|
|
105
|
+
expect(buildPaneSharePayload(pluginRegistry, {
|
|
106
|
+
instanceId: "missing:main",
|
|
107
|
+
paneId: "missing",
|
|
108
|
+
})).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
publishableMarketplacePane,
|
|
3
|
+
} from "../layout-marketplace/payload";
|
|
4
|
+
import type { PluginRegistry } from "../plugins/registry";
|
|
5
|
+
import type { PaneInstanceConfig } from "../types/config";
|
|
6
|
+
import { parseSharePayload, type PaneShareData, type SharePayload } from "./payload";
|
|
7
|
+
|
|
8
|
+
export function buildPaneSharePayload(
|
|
9
|
+
pluginRegistry: PluginRegistry,
|
|
10
|
+
pane: PaneInstanceConfig,
|
|
11
|
+
paneState: Record<string, unknown> = {},
|
|
12
|
+
resolvedTicker?: string | null,
|
|
13
|
+
): Extract<SharePayload, { kind: "pane" }> | null {
|
|
14
|
+
const def = pluginRegistry.panes.get(pane.paneId);
|
|
15
|
+
if (!def) return null;
|
|
16
|
+
try {
|
|
17
|
+
const layout = publishableMarketplacePane(
|
|
18
|
+
pane,
|
|
19
|
+
paneState,
|
|
20
|
+
pluginRegistry.panes,
|
|
21
|
+
resolvedTicker,
|
|
22
|
+
);
|
|
23
|
+
const instance = layout.layout.instances[0]!;
|
|
24
|
+
const description = [...pluginRegistry.paneTemplates.values()]
|
|
25
|
+
.find((template) => template.paneId === pane.paneId)?.description;
|
|
26
|
+
const payload = parseSharePayload({
|
|
27
|
+
kind: "pane",
|
|
28
|
+
data: {
|
|
29
|
+
version: 2,
|
|
30
|
+
title: instance.title ?? def.name,
|
|
31
|
+
...(description ? { description } : {}),
|
|
32
|
+
layout,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return payload?.kind === "pane" ? payload : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function openPaneShare(
|
|
42
|
+
pluginRegistry: PluginRegistry,
|
|
43
|
+
share: PaneShareData,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
if (share.version === 2) {
|
|
46
|
+
await pluginRegistry.openPortablePaneShareAsyncFn(share.layout);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const template = pluginRegistry.paneTemplates.get(share.templateId);
|
|
50
|
+
if (!template?.publicShare) throw new Error("This shared pane is unavailable in this version of Gloomberb.");
|
|
51
|
+
const options = template.publicShare.restore(share.data);
|
|
52
|
+
if (!options) throw new Error("This shared pane contains invalid settings.");
|
|
53
|
+
await pluginRegistry.createPaneFromTemplateAsyncFn(template.id, options);
|
|
54
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMarketplaceLayoutPayload,
|
|
3
|
+
type LayoutMarketplacePayload,
|
|
4
|
+
} from "../layout-marketplace/payload";
|
|
5
|
+
import { safeExternalUrl } from "../utils/external-url";
|
|
6
|
+
|
|
7
|
+
export const MAX_SHARE_BYTES = 128 * 1024;
|
|
8
|
+
const MAX_TITLE_LENGTH = 200;
|
|
9
|
+
const MAX_TEXT_LENGTH = 50_000;
|
|
10
|
+
const MAX_TABLE_COLUMNS = 20;
|
|
11
|
+
const MAX_TABLE_ROWS = 200;
|
|
12
|
+
const MAX_CHART_SERIES = 20;
|
|
13
|
+
const MAX_CHART_POINTS = 500;
|
|
14
|
+
const MAX_PANE_DESCRIPTION_LENGTH = 500;
|
|
15
|
+
const MAX_PANE_DATA_DEPTH = 8;
|
|
16
|
+
const MAX_PANE_OBJECT_KEYS = 64;
|
|
17
|
+
const MAX_PANE_ARRAY_ITEMS = 5_000;
|
|
18
|
+
const MAX_PANE_STRING_LENGTH = 4_096;
|
|
19
|
+
const PANE_TEMPLATE_ID = /^[a-z0-9][a-z0-9._:-]{0,119}$/;
|
|
20
|
+
|
|
21
|
+
type CellValue = string | number | boolean | null;
|
|
22
|
+
export type ShareJsonValue = CellValue | ShareJsonValue[] | { [key: string]: ShareJsonValue };
|
|
23
|
+
|
|
24
|
+
export interface TableShareData {
|
|
25
|
+
title: string;
|
|
26
|
+
columns: Array<{ key: string; label: string }>;
|
|
27
|
+
rows: Array<Record<string, CellValue>>;
|
|
28
|
+
sourceUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChartShareData {
|
|
32
|
+
title: string;
|
|
33
|
+
series: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
points: Array<{ x: string | number; y: number }>;
|
|
36
|
+
}>;
|
|
37
|
+
sourceUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ArticleShareData {
|
|
41
|
+
title: string;
|
|
42
|
+
text: string;
|
|
43
|
+
sourceUrl?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LegacyPaneShareData {
|
|
47
|
+
version: 1;
|
|
48
|
+
templateId: string;
|
|
49
|
+
title: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
data: Record<string, ShareJsonValue>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PortablePaneShareData {
|
|
55
|
+
version: 2;
|
|
56
|
+
title: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
layout: LayoutMarketplacePayload;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type PaneShareData = LegacyPaneShareData | PortablePaneShareData;
|
|
62
|
+
|
|
63
|
+
export type SharePayload =
|
|
64
|
+
| { kind: "table"; data: TableShareData }
|
|
65
|
+
| { kind: "chart"; data: ChartShareData }
|
|
66
|
+
| { kind: "article"; data: ArticleShareData }
|
|
67
|
+
| { kind: "pane"; data: PaneShareData };
|
|
68
|
+
|
|
69
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shortString(value: unknown, max = MAX_TITLE_LENGTH): value is string {
|
|
74
|
+
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function safeOptionalUrl(value: unknown): value is string | undefined {
|
|
78
|
+
return value === undefined || (typeof value === "string" && safeExternalUrl(value) !== null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isCell(value: unknown): value is CellValue {
|
|
82
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isTableData(value: unknown): value is TableShareData {
|
|
86
|
+
if (!record(value) || !shortString(value.title) || !safeOptionalUrl(value.sourceUrl)) return false;
|
|
87
|
+
if (!Array.isArray(value.columns) || value.columns.length === 0 || value.columns.length > MAX_TABLE_COLUMNS) return false;
|
|
88
|
+
const keys = new Set<string>();
|
|
89
|
+
for (const column of value.columns) {
|
|
90
|
+
if (!record(column) || !shortString(column.key, 80) || !shortString(column.label, 120) || keys.has(column.key)) return false;
|
|
91
|
+
keys.add(column.key);
|
|
92
|
+
}
|
|
93
|
+
return Array.isArray(value.rows)
|
|
94
|
+
&& value.rows.length <= MAX_TABLE_ROWS
|
|
95
|
+
&& value.rows.every((row) => record(row)
|
|
96
|
+
&& Object.keys(row).every((key) => keys.has(key))
|
|
97
|
+
&& Object.values(row).every(isCell));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isChartData(value: unknown): value is ChartShareData {
|
|
101
|
+
if (!record(value) || !shortString(value.title) || !safeOptionalUrl(value.sourceUrl)) return false;
|
|
102
|
+
return Array.isArray(value.series)
|
|
103
|
+
&& value.series.length > 0
|
|
104
|
+
&& value.series.length <= MAX_CHART_SERIES
|
|
105
|
+
&& value.series.every((series) => record(series)
|
|
106
|
+
&& shortString(series.name, 120)
|
|
107
|
+
&& Array.isArray(series.points)
|
|
108
|
+
&& series.points.length > 0
|
|
109
|
+
&& series.points.length <= MAX_CHART_POINTS
|
|
110
|
+
&& series.points.every((point) => record(point)
|
|
111
|
+
&& ((typeof point.x === "string" && point.x.length <= 100) || (typeof point.x === "number" && Number.isFinite(point.x)))
|
|
112
|
+
&& typeof point.y === "number"
|
|
113
|
+
&& Number.isFinite(point.y)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isArticleData(value: unknown): value is ArticleShareData {
|
|
117
|
+
return record(value)
|
|
118
|
+
&& shortString(value.title)
|
|
119
|
+
&& typeof value.text === "string"
|
|
120
|
+
&& value.text.length <= MAX_TEXT_LENGTH
|
|
121
|
+
&& safeOptionalUrl(value.sourceUrl);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function boundedJson(value: unknown, depth = 0): value is ShareJsonValue {
|
|
125
|
+
if (value === null || typeof value === "boolean") return true;
|
|
126
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
127
|
+
if (typeof value === "string") return value.length <= MAX_PANE_STRING_LENGTH;
|
|
128
|
+
if (depth >= MAX_PANE_DATA_DEPTH) return false;
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.length <= MAX_PANE_ARRAY_ITEMS && value.every((entry) => boundedJson(entry, depth + 1));
|
|
131
|
+
}
|
|
132
|
+
if (!record(value)) return false;
|
|
133
|
+
const entries = Object.entries(value);
|
|
134
|
+
return entries.length <= MAX_PANE_OBJECT_KEYS && entries.every(([, entry]) => boundedJson(entry, depth + 1));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parsePaneData(value: unknown): PaneShareData | null {
|
|
138
|
+
if (!record(value) || !shortString(value.title)) return null;
|
|
139
|
+
if (
|
|
140
|
+
value.version === 1
|
|
141
|
+
&& Object.keys(value).every((key) => ["version", "templateId", "title", "description", "data"].includes(key))
|
|
142
|
+
&& typeof value.templateId === "string"
|
|
143
|
+
&& PANE_TEMPLATE_ID.test(value.templateId)
|
|
144
|
+
&& (value.description === undefined || shortString(value.description, MAX_PANE_DESCRIPTION_LENGTH))
|
|
145
|
+
&& record(value.data)
|
|
146
|
+
&& boundedJson(value.data)
|
|
147
|
+
) return value as unknown as LegacyPaneShareData;
|
|
148
|
+
if (
|
|
149
|
+
value.version !== 2
|
|
150
|
+
|| !Object.keys(value).every((key) => ["version", "title", "description", "layout"].includes(key))
|
|
151
|
+
|| (value.description !== undefined && !shortString(value.description, MAX_PANE_DESCRIPTION_LENGTH))
|
|
152
|
+
) return null;
|
|
153
|
+
const layout = parseMarketplaceLayoutPayload(value.layout);
|
|
154
|
+
return layout?.schemaVersion === 2 && layout.layout.instances.length === 1
|
|
155
|
+
? {
|
|
156
|
+
version: 2,
|
|
157
|
+
title: value.title,
|
|
158
|
+
...(value.description === undefined ? {} : { description: value.description }),
|
|
159
|
+
layout,
|
|
160
|
+
}
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function parseSharePayload(value: unknown): SharePayload | null {
|
|
165
|
+
if (!record(value) || !shortString(value.kind, 20) || !("data" in value)) return null;
|
|
166
|
+
let json: string;
|
|
167
|
+
try { json = JSON.stringify(value); } catch { return null; }
|
|
168
|
+
if (new TextEncoder().encode(json).byteLength > MAX_SHARE_BYTES) return null;
|
|
169
|
+
if (value.kind === "table" && isTableData(value.data)) return value as unknown as SharePayload;
|
|
170
|
+
if (value.kind === "chart" && isChartData(value.data)) return value as unknown as SharePayload;
|
|
171
|
+
if (value.kind === "article" && isArticleData(value.data)) return value as unknown as SharePayload;
|
|
172
|
+
if (value.kind === "pane") {
|
|
173
|
+
const data = parsePaneData(value.data);
|
|
174
|
+
if (data) return { kind: "pane", data };
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
@@ -18,6 +18,7 @@ const verifiedUser: AuthUser = {
|
|
|
18
18
|
const originalEnsureVerifiedSession = apiClient.ensureVerifiedSession.bind(apiClient);
|
|
19
19
|
const originalGetCloudHistory = apiClient.getCloudHistory.bind(apiClient);
|
|
20
20
|
const originalGetCloudQuote = apiClient.getCloudQuote.bind(apiClient);
|
|
21
|
+
const originalGetCloudExchangeRate = apiClient.getCloudExchangeRate.bind(apiClient);
|
|
21
22
|
const originalGetCloudHolders = apiClient.getCloudHolders.bind(apiClient);
|
|
22
23
|
const originalGetCloudAnalystResearch = apiClient.getCloudAnalystResearch.bind(apiClient);
|
|
23
24
|
const originalGetCloudCorporateActions = apiClient.getCloudCorporateActions.bind(apiClient);
|
|
@@ -88,6 +89,7 @@ afterEach(() => {
|
|
|
88
89
|
apiClient.ensureVerifiedSession = originalEnsureVerifiedSession;
|
|
89
90
|
apiClient.getCloudHistory = originalGetCloudHistory;
|
|
90
91
|
apiClient.getCloudQuote = originalGetCloudQuote;
|
|
92
|
+
apiClient.getCloudExchangeRate = originalGetCloudExchangeRate;
|
|
91
93
|
apiClient.getCloudHolders = originalGetCloudHolders;
|
|
92
94
|
apiClient.getCloudAnalystResearch = originalGetCloudAnalystResearch;
|
|
93
95
|
apiClient.getCloudCorporateActions = originalGetCloudCorporateActions;
|
|
@@ -98,7 +100,65 @@ afterEach(() => {
|
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
describe("GloomberbCloudProvider", () => {
|
|
101
|
-
test("
|
|
103
|
+
test("uses public delayed market routes anonymously but keeps research protected", async () => {
|
|
104
|
+
let sessionChecks = 0;
|
|
105
|
+
apiClient.ensureVerifiedSession = async () => {
|
|
106
|
+
sessionChecks += 1;
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
apiClient.getCloudQuote = async () => ({
|
|
110
|
+
status: "success",
|
|
111
|
+
data: {
|
|
112
|
+
symbol: "AAPL",
|
|
113
|
+
providerId: "gloomberb-cloud",
|
|
114
|
+
price: 200,
|
|
115
|
+
currency: "USD",
|
|
116
|
+
change: 1,
|
|
117
|
+
changePercent: 0.5,
|
|
118
|
+
lastUpdated: Date.now(),
|
|
119
|
+
dataSource: "delayed",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
apiClient.getCloudHistory = async () => ({
|
|
123
|
+
status: "success",
|
|
124
|
+
providerMeta: { provider: "yahoo" },
|
|
125
|
+
data: [{ date: "2026-08-21", close: 200 }],
|
|
126
|
+
});
|
|
127
|
+
apiClient.getCloudOptionsChain = async () => ({
|
|
128
|
+
status: "success",
|
|
129
|
+
data: {
|
|
130
|
+
providerId: "gloomberb-cloud",
|
|
131
|
+
underlyingSymbol: "AAPL",
|
|
132
|
+
expirationDates: [1_800_000_000],
|
|
133
|
+
calls: [],
|
|
134
|
+
puts: [],
|
|
135
|
+
dataSource: "delayed",
|
|
136
|
+
feed: "yahoo",
|
|
137
|
+
delayMinutes: 15,
|
|
138
|
+
realtimeEligible: false,
|
|
139
|
+
asOf: "2026-08-21T20:00:00.000Z",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
apiClient.getCloudExchangeRate = async () => ({
|
|
143
|
+
status: "success",
|
|
144
|
+
data: { rate: 1.25 },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const provider = new GloomberbCloudProvider();
|
|
148
|
+
expect(await provider.canProvide()).toBe(true);
|
|
149
|
+
expect((await provider.getQuote("AAPL", "NASDAQ")).price).toBe(200);
|
|
150
|
+
expect(await provider.getPriceHistory("AAPL", "NASDAQ", "1M")).toHaveLength(1);
|
|
151
|
+
expect((await provider.getOptionsChain("AAPL", "NASDAQ")).feed).toBe("yahoo");
|
|
152
|
+
expect(await provider.getExchangeRate("GBP")).toBe(1.25);
|
|
153
|
+
expect(sessionChecks).toBe(0);
|
|
154
|
+
|
|
155
|
+
await expect(provider.getAnalystResearch("AAPL", "NASDAQ")).rejects.toThrow(
|
|
156
|
+
"requires signup and email verification",
|
|
157
|
+
);
|
|
158
|
+
expect(sessionChecks).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("fetches detailed intraday chart history with cloud intervals", async () => {
|
|
102
162
|
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
103
163
|
|
|
104
164
|
const requestArgs: { current: { symbol: string; exchange: string; params: Record<string, string | number | undefined> } | null } = { current: null };
|